/** * CSV Import Script * Imports OptionsFlow, OptionsVolume, and OptionsOpenInterest CSV files into database * * Usage: node scripts/importCSV.js */ import fs from 'fs'; import { parse } from 'csv-parse/sync'; import { rawQuery } from '../src/db.js'; import path from 'path'; import { fileURLToPath } from 'url'; import dotenv from 'dotenv'; // Load environment variables dotenv.config(); // Ensure we're using local DB for imports if (process.env.USE_LOCAL_DB !== 'true' && !process.env.DATABASE_URL) { console.warn('āš ļø Warning: Not using local database. CSV import works best with local PostgreSQL.'); console.warn(' Set USE_LOCAL_DB=true in .env for local development.'); } const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * Parse CSV file */ function parseCSV(filePath) { try { const fileContent = fs.readFileSync(filePath, 'utf-8'); const records = parse(fileContent, { columns: true, skip_empty_lines: true, trim: true, cast: true }); return records; } catch (error) { console.error(`Error reading ${filePath}:`, error.message); throw error; } } /** * Normalize date string to PostgreSQL date format */ function normalizeDate(dateStr) { if (!dateStr) return null; // Handle MM/DD/YYYY format if (dateStr.match(/^\d{1,2}\/\d{1,2}\/\d{4}$/)) { const [month, day, year] = dateStr.split('/'); return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`; } // Handle YYYY-MM-DD format if (dateStr.match(/^\d{4}-\d{2}-\d{2}$/)) { return dateStr; } return null; } /** * Normalize time string * Fixes common issues like "34 AM" (missing hour) or ":34 AM" (missing hour with colon) */ function normalizeTime(timeStr) { if (!timeStr) return null; let time = timeStr.trim(); // Fix pattern: ":34 AM" or ":34 PM" (missing hour, has colon) // Convert to "9:34 AM" or "1:34 PM" if (time.match(/^:\d{1,2}\s*(AM|PM)$/i)) { const match = time.match(/^:(\d{1,2})\s*(AM|PM)$/i); if (match) { const minute = match[1].padStart(2, '0'); const ampm = match[2].toUpperCase(); const hour = ampm === 'AM' ? '9' : '1'; // Default hour time = `${hour}:${minute}:00 ${ampm}`; } } // Fix pattern: "34 AM" or "34 PM" (missing hour, no colon) // If number > 12, it's definitely minutes, add default hour // If number <= 12, it could be hour or minute - default to minute with hour 9/1 if (time.match(/^\d{1,2}\s*(AM|PM)$/i) && !time.includes(':')) { const match = time.match(/^(\d{1,2})\s*(AM|PM)$/i); if (match) { const num = parseInt(match[1]); const ampm = match[2].toUpperCase(); if (num > 12) { // Definitely minutes (can't be hour > 12) const minute = match[1].padStart(2, '0'); const hour = ampm === 'AM' ? '9' : '1'; time = `${hour}:${minute}:00 ${ampm}`; } else { // Could be hour or minute - assume it's minute with default hour const minute = match[1].padStart(2, '0'); const hour = ampm === 'AM' ? '9' : '1'; time = `${hour}:${minute}:00 ${ampm}`; } } } return time; } /** * Clean numeric value */ function cleanNumeric(value) { if (!value || value === '' || value === null || value === undefined) return null; if (typeof value === 'number') return value; // Remove commas, dollar signs, spaces const cleaned = String(value).replace(/[\s$,]/g, ''); const num = parseFloat(cleaned); return isNaN(num) ? null : num; } /** * Import OptionsFlow CSV */ async function importOptionsFlow(csvPath) { console.log('šŸ“„ Importing OptionsFlow data...'); const records = parseCSV(csvPath); console.log(` Found ${records.length} records`); // Ensure table exists const createTableSQL = ` CREATE TABLE IF NOT EXISTS "OptionsFlow_monthly" ( "CreatedDate" TEXT, "CreatedTime" TEXT, "Symbol" TEXT, "Type" TEXT, "Volume" TEXT, "Price" TEXT, "Side" TEXT, "CallPut" TEXT, "Strike" TEXT, "Spot" TEXT, "Premium" TEXT, "ExpirationDate" TEXT, "Color" TEXT, "ImpliedVolatility" TEXT, "Dte" TEXT, "ER" TEXT, "StockEtf" TEXT, "Sector" TEXT, "Uoa" TEXT, "Weekly" TEXT, "MktCap" TEXT, "OI" TEXT ); `; await rawQuery(createTableSQL); console.log(' āœ… Table created/verified'); // Batch insert (100 records at a time to avoid parameter limits) const batchSize = 100; let imported = 0; for (let i = 0; i < records.length; i += batchSize) { const batch = records.slice(i, i + batchSize); // Build parameterized query const placeholders = batch.map((_, idx) => { const start = idx * 22 + 1; return `($${start}, $${start + 1}, $${start + 2}, $${start + 3}, $${start + 4}, $${start + 5}, $${start + 6}, $${start + 7}, $${start + 8}, $${start + 9}, $${start + 10}, $${start + 11}, $${start + 12}, $${start + 13}, $${start + 14}, $${start + 15}, $${start + 16}, $${start + 17}, $${start + 18}, $${start + 19}, $${start + 20}, $${start + 21})`; }).join(', '); const params = batch.flatMap(row => [ row.CreatedDate || null, row.CreatedTime || null, row.Symbol || null, row.Type || null, row.Volume || null, row.Price || null, row.Side || null, row.CallPut || null, row.Strike || null, row.Spot || null, row.Premium || null, row.ExpirationDate || null, row.Color || null, row.ImpliedVolatility || null, row.Dte || null, row.ER || null, row.StockEtf || null, row.Sector || null, row.Uoa || null, row.Weekly || null, row.MktCap || null, row.OI || null ]); const insertSQL = ` INSERT INTO "OptionsFlow_monthly" ( "CreatedDate", "CreatedTime", "Symbol", "Type", "Volume", "Price", "Side", "CallPut", "Strike", "Spot", "Premium", "ExpirationDate", "Color", "ImpliedVolatility", "Dte", "ER", "StockEtf", "Sector", "Uoa", "Weekly", "MktCap", "OI" ) VALUES ${placeholders} `; try { await rawQuery(insertSQL, params); imported += batch.length; console.log(` āœ… Imported ${imported}/${records.length} records...`); } catch (error) { console.error(` āŒ Error importing batch ${Math.floor(i / batchSize) + 1}:`, error.message); throw error; } } console.log(` āœ… Successfully imported ${imported} OptionsFlow records`); return imported; } /** * Import OptionsVolume CSV */ async function importOptionsVolume(csvPath) { console.log('šŸ“„ Importing OptionsVolume data...'); const records = parseCSV(csvPath); console.log(` Found ${records.length} records`); // Create table if it doesn't exist const createTableSQL = ` CREATE TABLE IF NOT EXISTS "OptionsVolume" ( "SYMBOL" TEXT, "VOL" NUMERIC, "C VOL" NUMERIC, "P VOL" NUMERIC, "P/C" NUMERIC, "AVG VOL" NUMERIC, "RATIO" NUMERIC, "COI" NUMERIC, "POI" NUMERIC, "DCOI" NUMERIC, "DPOI" NUMERIC ); `; await rawQuery(createTableSQL); console.log(' āœ… Table created/verified'); // Batch insert const batchSize = 100; let imported = 0; for (let i = 0; i < records.length; i += batchSize) { const batch = records.slice(i, i + batchSize); const placeholders = batch.map((_, idx) => { const start = idx * 11 + 1; return `($${start}, $${start + 1}, $${start + 2}, $${start + 3}, $${start + 4}, $${start + 5}, $${start + 6}, $${start + 7}, $${start + 8}, $${start + 9}, $${start + 10})`; }).join(', '); const params = batch.flatMap(row => [ row.SYMBOL || null, cleanNumeric(row.VOL), cleanNumeric(row['C VOL']), cleanNumeric(row['P VOL']), cleanNumeric(row['P/C']), cleanNumeric(row['AVG VOL']), cleanNumeric(row.RATIO), cleanNumeric(row.COI), cleanNumeric(row.POI), cleanNumeric(row.DCOI), cleanNumeric(row.DPOI) ]); const insertSQL = ` INSERT INTO "OptionsVolume" ( "SYMBOL", "VOL", "C VOL", "P VOL", "P/C", "AVG VOL", "RATIO", "COI", "POI", "DCOI", "DPOI" ) VALUES ${placeholders} `; try { await rawQuery(insertSQL, params); imported += batch.length; console.log(` āœ… Imported ${imported}/${records.length} records...`); } catch (error) { console.error(` āŒ Error importing batch:`, error.message); throw error; } } console.log(` āœ… Successfully imported ${imported} OptionsVolume records`); return imported; } /** * Import OptionsOpenInterest CSV */ async function importOptionsOpenInterest(csvPath) { console.log('šŸ“„ Importing OptionsOpenInterest data...'); const records = parseCSV(csvPath); console.log(` Found ${records.length} records`); // Create table if it doesn't exist const createTableSQL = ` CREATE TABLE IF NOT EXISTS "OptionsOpenInterest" ( "SYMBOL" TEXT, "EXP" TEXT, "C/P" TEXT, "STRIKE" NUMERIC, "OI" NUMERIC, "DOI" NUMERIC ); `; await rawQuery(createTableSQL); console.log(' āœ… Table created/verified'); // Batch insert const batchSize = 100; let imported = 0; for (let i = 0; i < records.length; i += batchSize) { const batch = records.slice(i, i + batchSize); const placeholders = batch.map((_, idx) => { const start = idx * 6 + 1; return `($${start}, $${start + 1}, $${start + 2}, $${start + 3}, $${start + 4}, $${start + 5})`; }).join(', '); const params = batch.flatMap(row => [ row.SYMBOL || null, row.EXP || null, row['C/P'] || null, cleanNumeric(row.STRIKE), cleanNumeric(row.OI), cleanNumeric(row.DOI) ]); const insertSQL = ` INSERT INTO "OptionsOpenInterest" ( "SYMBOL", "EXP", "C/P", "STRIKE", "OI", "DOI" ) VALUES ${placeholders} `; try { await rawQuery(insertSQL, params); imported += batch.length; console.log(` āœ… Imported ${imported}/${records.length} records...`); } catch (error) { console.error(` āŒ Error importing batch:`, error.message); throw error; } } console.log(` āœ… Successfully imported ${imported} OptionsOpenInterest records`); return imported; } /** * Import AlertStream CSV */ async function importAlertStream(csvPath) { console.log('šŸ“„ Importing AlertStream data...'); const records = parseCSV(csvPath); console.log(` Found ${records.length} records`); // Ensure table exists (columns match what queries expect) const createTableSQL = ` DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema='public' AND table_name='AlertStream_monthly') THEN CREATE TABLE public."AlertStream_monthly" ( "Date" TEXT, "AlertDate" TEXT, "date" TEXT, "alert_date" TEXT, "Ticker" TEXT, "ticker" TEXT, "Symbol" TEXT, "symbol" TEXT, "Type" TEXT, "type" TEXT, "AlertType" TEXT, "alert_type" TEXT, "Message" TEXT, "message" TEXT, "Timestamp" TEXT, "timestamp" TEXT, "alert_time_txt" TEXT, "Notional" TEXT, "notional" TEXT, "Pct_of_Avg30Day" TEXT, "pct_of_avg30day" TEXT, "Pct_of_Avg30" TEXT, "pct_of_avg30" TEXT, "Price" NUMERIC(12, 4), "price" NUMERIC(12, 4), "Volume" NUMERIC(15, 2), "volume" NUMERIC(15, 2) ); END IF; END $$; `; await rawQuery(createTableSQL); // Add missing columns if they don't exist const addColumnSQL = ` DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='AlertStream_monthly' AND column_name='Ticker') THEN ALTER TABLE public."AlertStream_monthly" ADD COLUMN "Ticker" TEXT; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='AlertStream_monthly' AND column_name='ticker') THEN ALTER TABLE public."AlertStream_monthly" ADD COLUMN "ticker" TEXT; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='AlertStream_monthly' AND column_name='Timestamp') THEN ALTER TABLE public."AlertStream_monthly" ADD COLUMN "Timestamp" TEXT; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='AlertStream_monthly' AND column_name='timestamp') THEN ALTER TABLE public."AlertStream_monthly" ADD COLUMN "timestamp" TEXT; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='AlertStream_monthly' AND column_name='Type') THEN ALTER TABLE public."AlertStream_monthly" ADD COLUMN "Type" TEXT; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='AlertStream_monthly' AND column_name='type') THEN ALTER TABLE public."AlertStream_monthly" ADD COLUMN "type" TEXT; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='AlertStream_monthly' AND column_name='Message') THEN ALTER TABLE public."AlertStream_monthly" ADD COLUMN "Message" TEXT; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='AlertStream_monthly' AND column_name='message') THEN ALTER TABLE public."AlertStream_monthly" ADD COLUMN "message" TEXT; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='AlertStream_monthly' AND column_name='Date') THEN ALTER TABLE public."AlertStream_monthly" ADD COLUMN "Date" TEXT; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='AlertStream_monthly' AND column_name='date') THEN ALTER TABLE public."AlertStream_monthly" ADD COLUMN "date" TEXT; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='AlertStream_monthly' AND column_name='Price') THEN ALTER TABLE public."AlertStream_monthly" ADD COLUMN "Price" NUMERIC(12, 4); END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='AlertStream_monthly' AND column_name='price') THEN ALTER TABLE public."AlertStream_monthly" ADD COLUMN "price" NUMERIC(12, 4); END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='AlertStream_monthly' AND column_name='Volume') THEN ALTER TABLE public."AlertStream_monthly" ADD COLUMN "Volume" NUMERIC(15, 2); END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='AlertStream_monthly' AND column_name='volume') THEN ALTER TABLE public."AlertStream_monthly" ADD COLUMN "volume" NUMERIC(15, 2); END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='AlertStream_monthly' AND column_name='Notional') THEN ALTER TABLE public."AlertStream_monthly" ADD COLUMN "Notional" TEXT; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='AlertStream_monthly' AND column_name='notional') THEN ALTER TABLE public."AlertStream_monthly" ADD COLUMN "notional" TEXT; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='AlertStream_monthly' AND column_name='Pct_of_Avg30Day') THEN ALTER TABLE public."AlertStream_monthly" ADD COLUMN "Pct_of_Avg30Day" TEXT; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='AlertStream_monthly' AND column_name='pct_of_avg30day') THEN ALTER TABLE public."AlertStream_monthly" ADD COLUMN "pct_of_avg30day" TEXT; END IF; END $$; `; await rawQuery(addColumnSQL); console.log(' āœ… Table created/verified'); // Batch insert const batchSize = 100; let imported = 0; for (let i = 0; i < records.length; i += batchSize) { const batch = records.slice(i, i + batchSize); // Map CSV columns to both CamelCase and lowercase versions for compatibility const placeholders = batch.map((_, idx) => { const start = idx * 18 + 1; return `($${start}, $${start + 1}, $${start + 2}, $${start + 3}, $${start + 4}, $${start + 5}, $${start + 6}, $${start + 7}, $${start + 8}, $${start + 9}, $${start + 10}, $${start + 11}, $${start + 12}, $${start + 13}, $${start + 14}, $${start + 15}, $${start + 16}, $${start + 17})`; }).join(', '); const params = batch.flatMap(row => [ row.Date || null, row.Date || null, // Date, date row.Timestamp || null, row.Timestamp || null, // Timestamp, timestamp row.Ticker || null, row.Ticker || null, // Ticker, ticker row.Type || null, row.Type || null, // Type, type row.Message || null, row.Message || null, // Message, message cleanNumeric(row.Price), cleanNumeric(row.Price), // Price, price cleanNumeric(row.Volume), cleanNumeric(row.Volume), // Volume, volume cleanNumeric(row.Notional)?.toString() || null, cleanNumeric(row.Notional)?.toString() || null, // Notional, notional row.Pct_of_Avg30Day || null, row.Pct_of_Avg30Day || null // Pct_of_Avg30Day, pct_of_avg30day ]); const insertSQL = ` INSERT INTO public."AlertStream_monthly" ( "Date", "date", "Timestamp", "timestamp", "Ticker", "ticker", "Type", "type", "Message", "message", "Price", "price", "Volume", "volume", "Notional", "notional", "Pct_of_Avg30Day", "pct_of_avg30day" ) VALUES ${placeholders} `; try { await rawQuery(insertSQL, params); imported += batch.length; console.log(` āœ… Imported ${imported}/${records.length} records...`); } catch (error) { console.error(` āŒ Error importing batch:`, error.message); throw error; } } console.log(` āœ… Successfully imported ${imported} AlertStream records`); return imported; } /** * Main import function */ async function main() { console.log('šŸš€ Starting CSV import process...\n'); try { // Find CSV files in data folder const projectRoot = path.resolve(__dirname, '../..'); const dataFolder = path.join(projectRoot, 'data'); // Check if data folder exists if (!fs.existsSync(dataFolder)) { throw new Error(`Data folder not found at ${dataFolder}`); } // Find all CSV files const optionsFlowFiles = [ path.join(dataFolder, 'OptionsFlow.csv'), path.join(dataFolder, 'OptionsFlowMonthly (1).csv'), path.join(dataFolder, 'HistoricalOptionsFlow.csv') ].filter(f => fs.existsSync(f)); const optionsVolumePath = path.join(dataFolder, 'OptionsVolume.csv'); const optionsOpenInterestPath = path.join(dataFolder, 'OptionsOpenInterest.csv'); const alertStreamPath = path.join(dataFolder, 'AlertStream.csv'); console.log('šŸ“ Found CSV files:'); optionsFlowFiles.forEach(f => console.log(` - ${f}`)); if (fs.existsSync(optionsVolumePath)) console.log(` - ${optionsVolumePath}`); if (fs.existsSync(optionsOpenInterestPath)) console.log(` - ${optionsOpenInterestPath}`); if (fs.existsSync(alertStreamPath)) console.log(` - ${alertStreamPath}`); console.log(''); // Import each file const results = { optionsFlow: 0, optionsVolume: 0, optionsOpenInterest: 0, alertStream: 0 }; // Import all OptionsFlow files for (const filePath of optionsFlowFiles) { console.log(`šŸ“„ Importing ${path.basename(filePath)}...`); const count = await importOptionsFlow(filePath); results.optionsFlow += count; console.log(''); } if (fs.existsSync(optionsVolumePath)) { results.optionsVolume = await importOptionsVolume(optionsVolumePath); console.log(''); } if (fs.existsSync(optionsOpenInterestPath)) { results.optionsOpenInterest = await importOptionsOpenInterest(optionsOpenInterestPath); console.log(''); } if (fs.existsSync(alertStreamPath)) { results.alertStream = await importAlertStream(alertStreamPath); console.log(''); } // Summary console.log('āœ… Import Summary:'); console.log(` OptionsFlow: ${results.optionsFlow} records (from ${optionsFlowFiles.length} file(s))`); if (results.optionsVolume > 0) console.log(` OptionsVolume: ${results.optionsVolume} records`); if (results.optionsOpenInterest > 0) console.log(` OptionsOpenInterest: ${results.optionsOpenInterest} records`); if (results.alertStream > 0) console.log(` AlertStream: ${results.alertStream} records`); const total = results.optionsFlow + results.optionsVolume + results.optionsOpenInterest + results.alertStream; console.log(` Total: ${total} records`); } catch (error) { console.error('\nāŒ Import failed:', error.message); console.error(error); process.exit(1); } } // Run if called directly const isMainModule = import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith('importCSV.js') || process.argv[1]?.endsWith('importCSV.js'); if (isMainModule || !process.env.NODE_ENV) { main().catch(error => { console.error('Fatal error:', error); process.exit(1); }); } export { importOptionsFlow, importOptionsVolume, importOptionsOpenInterest, importAlertStream };