304 lines
10 KiB
JavaScript
304 lines
10 KiB
JavaScript
/**
|
|
* 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 };
|
|
|