/** * Sync BlackBox Stocks Options Flow Data * Fetches latest flow data from BlackBox API and updates the database * * Usage: * node scripts/syncBlackBoxFlow.js * node scripts/syncBlackBoxFlow.js --start-date 2024-01-15 --end-date 2024-01-16 * node scripts/syncBlackBoxFlow.js --limit 100 */ import { fetchBlackBoxFlow, mapBlackBoxToDatabase } from '../src/services/blackboxApiService.js'; import { rawQuery, testConnection } from '../src/db.js'; import dotenv from 'dotenv'; dotenv.config(); /** * Ensure OptionsFlow_monthly table exists */ async function ensureTableExists() { 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 OptionsFlow_monthly verified'); } /** * Insert flow records into database * @param {Array} records - Array of mapped records * @param {boolean} skipDuplicates - Skip duplicate records based on key fields */ async function insertFlowRecords(records, skipDuplicates = true) { if (records.length === 0) { console.log(' No records to insert'); return 0; } const batchSize = 100; let inserted = 0; let skipped = 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 = skipDuplicates ? ` 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} ON CONFLICT DO NOTHING ` : ` 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); inserted += batch.length; console.log(` ✅ Inserted batch ${Math.floor(i / batchSize) + 1}: ${batch.length} records (Total: ${inserted})`); } catch (error) { // If ON CONFLICT doesn't work (SQLite), try without it if (error.message.includes('ON CONFLICT') || error.message.includes('syntax error')) { console.log(' ⚠️ ON CONFLICT not supported, checking for duplicates manually...'); // For databases without ON CONFLICT, we'll insert and let duplicates exist // or implement a check before insert const simpleInsertSQL = ` 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} `; await rawQuery(simpleInsertSQL, params); inserted += batch.length; console.log(` ✅ Inserted batch ${Math.floor(i / batchSize) + 1}: ${batch.length} records (Total: ${inserted})`); } else { console.error(` ❌ Error inserting batch ${Math.floor(i / batchSize) + 1}:`, error.message); throw error; } } } return inserted; } /** * Main sync function */ async function syncBlackBoxFlow() { console.log('🚀 Starting BlackBox Stocks flow data sync...\n'); try { // Test database connection first console.log('🔌 Testing database connection...'); const dbConnected = await testConnection(); if (!dbConnected) { console.error('❌ Database connection failed. Please check your .env configuration.'); process.exit(1); } console.log(''); // Parse command line arguments const args = process.argv.slice(2); const options = {}; for (let i = 0; i < args.length; i++) { if (args[i] === '--start-date' && args[i + 1]) { options.startDate = args[i + 1]; i++; } else if (args[i] === '--end-date' && args[i + 1]) { options.endDate = args[i + 1]; i++; } else if (args[i] === '--limit' && args[i + 1]) { options.count = parseInt(args[i + 1]); i++; } else if (args[i] === '--count' && args[i + 1]) { options.count = parseInt(args[i + 1]); i++; } else if (args[i] === '--symbol' && args[i + 1]) { options.symbol = args[i + 1]; i++; } } // Default to today if no dates provided if (!options.startDate && !options.endDate) { const today = new Date().toISOString().split('T')[0]; options.startDate = today; options.endDate = today; console.log(`📅 No date range specified, using today: ${today}`); } console.log(`📥 Fetching flow data from BlackBox API...`); console.log(` Options:`, options); // Ensure table exists await ensureTableExists(); // Fetch data from API const apiRecords = await fetchBlackBoxFlow(options); console.log(` ✅ Fetched ${apiRecords.length} records from API`); if (apiRecords.length === 0) { console.log('⚠️ No records returned from API'); return; } // Log sample record to understand structure if (apiRecords.length > 0) { console.log('\n📋 Sample API record structure:'); console.log(JSON.stringify(apiRecords[0], null, 2)); } // Map API records to database schema console.log('\n🔄 Mapping records to database schema...'); const mappedRecords = apiRecords.map(mapBlackBoxToDatabase); console.log(` ✅ Mapped ${mappedRecords.length} records`); // Log sample mapped record if (mappedRecords.length > 0) { console.log('\n📋 Sample mapped record:'); console.log(JSON.stringify(mappedRecords[0], null, 2)); } // Insert into database console.log('\n💾 Inserting records into database...'); const dbHost = process.env.LOCAL_DB_HOST || '100.121.163.23'; const dbName = process.env.LOCAL_DB_NAME || 'institutional_trader'; console.log(` Target: ${dbHost}/${dbName}`); const inserted = await insertFlowRecords(mappedRecords, true); console.log(`\n✅ Successfully synced ${inserted} records to ${dbHost}/${dbName}`); // Summary console.log('\n📊 Sync Summary:'); console.log(` Fetched from API: ${apiRecords.length} records`); console.log(` Inserted into DB: ${inserted} records`); // Get total count in database const totalCount = await rawQuery('SELECT COUNT(*) as count FROM "OptionsFlow_monthly"'); console.log(` Total records in DB: ${totalCount[0]?.count || 0}`); } catch (error) { console.error('\n❌ Sync 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('syncBlackBoxFlow.js') || process.argv[1]?.endsWith('syncBlackBoxFlow.js'); if (isMainModule || !process.env.NODE_ENV) { syncBlackBoxFlow().catch(error => { console.error('Fatal error:', error); process.exit(1); }); } export { syncBlackBoxFlow };