/** * 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 } from '../src/db.js'; import dotenv from 'dotenv'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; // Get the directory of the current module const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Load .env from backend root (parent directory of scripts) dotenv.config({ path: join(__dirname, '..', '.env') }); /** * 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; let errors = []; 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 { // Try with ON CONFLICT first (if unique constraint exists) await rawQuery(insertSQL, params); // Note: rawQuery returns rows array, not rowCount // We verify actual inserts by checking count before/after inserted += batch.length; console.log(` ✅ Inserted batch ${Math.floor(i / batchSize) + 1}: ${batch.length} records (Total: ${inserted})`); } catch (error) { // If ON CONFLICT fails (no unique constraint), use simple INSERT if (error.message.includes('ON CONFLICT') || error.message.includes('syntax error') || error.message.includes('there is no unique or exclusion constraint')) { console.log(` ⚠️ ON CONFLICT not supported (no unique constraint), using simple 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} `; try { await rawQuery(simpleInsertSQL, params); inserted += batch.length; console.log(` ✅ Inserted batch ${Math.floor(i / batchSize) + 1}: ${batch.length} records (Total: ${inserted})`); } catch (insertError) { console.error(` ❌ Error inserting batch ${Math.floor(i / batchSize) + 1}:`, insertError.message); errors.push({ batch: Math.floor(i / batchSize) + 1, error: insertError.message }); // Continue with next batch instead of throwing skipped += batch.length; } } else { console.error(` ❌ Error inserting batch ${Math.floor(i / batchSize) + 1}:`, error.message); errors.push({ batch: Math.floor(i / batchSize) + 1, error: error.message }); // Continue with next batch instead of throwing skipped += batch.length; } } } if (skipped > 0) { console.log(`\n⚠️ Skipped ${skipped} records due to errors`); if (errors.length > 0) { console.log(' Error summary:'); errors.slice(0, 5).forEach(e => console.log(` - Batch ${e.batch}: ${e.error}`)); if (errors.length > 5) { console.log(` ... and ${errors.length - 5} more errors`); } } } return inserted; } /** * Main sync function */ async function syncBlackBoxFlow() { console.log('🚀 Starting BlackBox Stocks flow data sync...\n'); try { // 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)); } // Get count before insert const countBefore = await rawQuery('SELECT COUNT(*) as count FROM "OptionsFlow_monthly"'); const beforeCount = parseInt(countBefore[0]?.count || 0); console.log(`\n📊 Records in DB before insert: ${beforeCount}`); // Insert into database console.log('\n💾 Inserting records into database...'); const inserted = await insertFlowRecords(mappedRecords, true); // Get count after insert to verify const countAfter = await rawQuery('SELECT COUNT(*) as count FROM "OptionsFlow_monthly"'); const afterCount = parseInt(countAfter[0]?.count || 0); const actuallyInserted = afterCount - beforeCount; console.log(`\n✅ Insert operation completed`); console.log(` Attempted to insert: ${inserted} records`); console.log(` Actually inserted: ${actuallyInserted} records`); // Summary console.log('\n📊 Sync Summary:'); console.log(` Fetched from API: ${apiRecords.length} records`); console.log(` Attempted inserts: ${inserted} records`); console.log(` Actually inserted: ${actuallyInserted} records`); console.log(` Records before: ${beforeCount}`); console.log(` Records after: ${afterCount}`); console.log(` Total records in DB: ${afterCount}`); if (actuallyInserted < inserted) { const diff = inserted - actuallyInserted; console.log(`\n⚠️ Warning: ${diff} records may have been duplicates or failed to insert`); } } 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 };