bulltrackers-module 1.0.733 → 1.0.734

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/functions/computation-system-v2/README.md +152 -0
  2. package/functions/computation-system-v2/computations/PopularInvestorProfileMetrics.js +720 -0
  3. package/functions/computation-system-v2/computations/PopularInvestorRiskAssessment.js +176 -0
  4. package/functions/computation-system-v2/computations/PopularInvestorRiskMetrics.js +294 -0
  5. package/functions/computation-system-v2/computations/TestComputation.js +46 -0
  6. package/functions/computation-system-v2/computations/UserPortfolioSummary.js +172 -0
  7. package/functions/computation-system-v2/config/bulltrackers.config.js +317 -0
  8. package/functions/computation-system-v2/framework/core/Computation.js +73 -0
  9. package/functions/computation-system-v2/framework/core/Manifest.js +223 -0
  10. package/functions/computation-system-v2/framework/core/RuleInjector.js +53 -0
  11. package/functions/computation-system-v2/framework/core/Rules.js +231 -0
  12. package/functions/computation-system-v2/framework/core/RunAnalyzer.js +163 -0
  13. package/functions/computation-system-v2/framework/cost/CostTracker.js +154 -0
  14. package/functions/computation-system-v2/framework/data/DataFetcher.js +399 -0
  15. package/functions/computation-system-v2/framework/data/QueryBuilder.js +232 -0
  16. package/functions/computation-system-v2/framework/data/SchemaRegistry.js +287 -0
  17. package/functions/computation-system-v2/framework/execution/Orchestrator.js +498 -0
  18. package/functions/computation-system-v2/framework/execution/TaskRunner.js +35 -0
  19. package/functions/computation-system-v2/framework/execution/middleware/CostTrackerMiddleware.js +32 -0
  20. package/functions/computation-system-v2/framework/execution/middleware/LineageMiddleware.js +32 -0
  21. package/functions/computation-system-v2/framework/execution/middleware/Middleware.js +14 -0
  22. package/functions/computation-system-v2/framework/execution/middleware/ProfilerMiddleware.js +47 -0
  23. package/functions/computation-system-v2/framework/index.js +45 -0
  24. package/functions/computation-system-v2/framework/lineage/LineageTracker.js +147 -0
  25. package/functions/computation-system-v2/framework/monitoring/Profiler.js +80 -0
  26. package/functions/computation-system-v2/framework/resilience/Checkpointer.js +66 -0
  27. package/functions/computation-system-v2/framework/scheduling/ScheduleValidator.js +327 -0
  28. package/functions/computation-system-v2/framework/storage/StateRepository.js +286 -0
  29. package/functions/computation-system-v2/framework/storage/StorageManager.js +469 -0
  30. package/functions/computation-system-v2/framework/storage/index.js +9 -0
  31. package/functions/computation-system-v2/framework/testing/ComputationTester.js +86 -0
  32. package/functions/computation-system-v2/framework/utils/Graph.js +205 -0
  33. package/functions/computation-system-v2/handlers/dispatcher.js +109 -0
  34. package/functions/computation-system-v2/handlers/index.js +23 -0
  35. package/functions/computation-system-v2/handlers/onDemand.js +289 -0
  36. package/functions/computation-system-v2/handlers/scheduler.js +327 -0
  37. package/functions/computation-system-v2/index.js +163 -0
  38. package/functions/computation-system-v2/rules/index.js +49 -0
  39. package/functions/computation-system-v2/rules/instruments.js +465 -0
  40. package/functions/computation-system-v2/rules/metrics.js +304 -0
  41. package/functions/computation-system-v2/rules/portfolio.js +534 -0
  42. package/functions/computation-system-v2/rules/rankings.js +655 -0
  43. package/functions/computation-system-v2/rules/social.js +562 -0
  44. package/functions/computation-system-v2/rules/trades.js +545 -0
  45. package/functions/computation-system-v2/scripts/migrate-sectors.js +73 -0
  46. package/functions/computation-system-v2/test/test-dispatcher.js +317 -0
  47. package/functions/computation-system-v2/test/test-framework.js +500 -0
  48. package/functions/computation-system-v2/test/test-real-execution.js +166 -0
  49. package/functions/computation-system-v2/test/test-real-integration.js +194 -0
  50. package/functions/computation-system-v2/test/test-refactor-e2e.js +131 -0
  51. package/functions/computation-system-v2/test/test-results.json +31 -0
  52. package/functions/computation-system-v2/test/test-risk-metrics-computation.js +329 -0
  53. package/functions/computation-system-v2/test/test-scheduler.js +204 -0
  54. package/functions/computation-system-v2/test/test-storage.js +449 -0
  55. package/functions/orchestrator/index.js +18 -26
  56. package/package.json +3 -2
@@ -0,0 +1,545 @@
1
+ /**
2
+ * @fileoverview Trade History Business Rules
3
+ *
4
+ * For extracting and analyzing trade_history_snapshots data.
5
+ * Each row contains PublicHistoryPositions - an array of closed trades.
6
+ *
7
+ * NOTE: Trade history shows individual closed positions. We can see:
8
+ * - Net profit percentage per trade
9
+ * - Open/close rates and dates
10
+ * - Leverage and direction
11
+ *
12
+ * We CANNOT determine the invested amount per position from this data alone.
13
+ *
14
+ * Usage in computation:
15
+ * ```javascript
16
+ * const trades = rules.trades.extractTrades(row);
17
+ * const stats = rules.trades.calculateTradeStats(trades);
18
+ * const byInstrument = rules.trades.groupByInstrument(trades);
19
+ * ```
20
+ */
21
+
22
+ // =============================================================================
23
+ // DATA EXTRACTION
24
+ // =============================================================================
25
+
26
+ /**
27
+ * Extract trades array from a trade_history_snapshots row.
28
+ * @param {Object} row - trade_history_snapshots row
29
+ * @returns {Array} Array of trade objects
30
+ */
31
+ function extractTrades(row) {
32
+ if (!row) return [];
33
+
34
+ let data = row.history_data;
35
+
36
+ if (typeof data === 'string') {
37
+ try {
38
+ data = JSON.parse(data);
39
+ } catch (e) {
40
+ return [];
41
+ }
42
+ }
43
+
44
+ return data?.PublicHistoryPositions || [];
45
+ }
46
+
47
+ /**
48
+ * Get user ID from trade history row.
49
+ * @param {Object} row - trade_history_snapshots row
50
+ * @returns {string|null}
51
+ */
52
+ function getUserId(row) {
53
+ if (!row) return null;
54
+
55
+ // Try row-level first
56
+ if (row.user_id) return String(row.user_id);
57
+
58
+ // Try from history_data
59
+ let data = row.history_data;
60
+ if (typeof data === 'string') {
61
+ try {
62
+ data = JSON.parse(data);
63
+ } catch (e) {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ return data?.cid ? String(data.cid) : null;
69
+ }
70
+
71
+ // =============================================================================
72
+ // TRADE PROPERTY ACCESSORS
73
+ // =============================================================================
74
+
75
+ /**
76
+ * Get instrument ID from a trade.
77
+ * @param {Object} trade - Trade object
78
+ * @returns {number|null}
79
+ */
80
+ function getInstrumentId(trade) {
81
+ return trade?.InstrumentID || null;
82
+ }
83
+
84
+ /**
85
+ * Get net profit percentage.
86
+ * @param {Object} trade - Trade object
87
+ * @returns {number}
88
+ */
89
+ function getNetProfit(trade) {
90
+ return trade?.NetProfit ?? 0;
91
+ }
92
+
93
+ /**
94
+ * Get open rate (price at open).
95
+ * @param {Object} trade - Trade object
96
+ * @returns {number}
97
+ */
98
+ function getOpenRate(trade) {
99
+ return trade?.OpenRate ?? 0;
100
+ }
101
+
102
+ /**
103
+ * Get close rate (price at close).
104
+ * @param {Object} trade - Trade object
105
+ * @returns {number}
106
+ */
107
+ function getCloseRate(trade) {
108
+ return trade?.CloseRate ?? 0;
109
+ }
110
+
111
+ /**
112
+ * Get open date.
113
+ * @param {Object} trade - Trade object
114
+ * @returns {Date|null}
115
+ */
116
+ function getOpenDate(trade) {
117
+ return trade?.OpenDateTime ? new Date(trade.OpenDateTime) : null;
118
+ }
119
+
120
+ /**
121
+ * Get close date.
122
+ * @param {Object} trade - Trade object
123
+ * @returns {Date|null}
124
+ */
125
+ function getCloseDate(trade) {
126
+ return trade?.CloseDateTime ? new Date(trade.CloseDateTime) : null;
127
+ }
128
+
129
+ /**
130
+ * Get leverage (integer).
131
+ * @param {Object} trade - Trade object
132
+ * @returns {number}
133
+ */
134
+ function getLeverage(trade) {
135
+ return trade?.Leverage ?? 1;
136
+ }
137
+
138
+ /**
139
+ * Check if trade was a buy (long).
140
+ * @param {Object} trade - Trade object
141
+ * @returns {boolean}
142
+ */
143
+ function isBuy(trade) {
144
+ return trade?.IsBuy === true;
145
+ }
146
+
147
+ /**
148
+ * Check if trade was a sell (short).
149
+ * @param {Object} trade - Trade object
150
+ * @returns {boolean}
151
+ */
152
+ function isSell(trade) {
153
+ return trade?.IsBuy === false;
154
+ }
155
+
156
+ /**
157
+ * Get position ID.
158
+ * @param {Object} trade - Trade object
159
+ * @returns {number|null}
160
+ */
161
+ function getPositionId(trade) {
162
+ return trade?.PositionID || null;
163
+ }
164
+
165
+ /**
166
+ * Get close reason code.
167
+ * Close reasons: 0 = manual, 5 = stop loss, etc.
168
+ * @param {Object} trade - Trade object
169
+ * @returns {number}
170
+ */
171
+ function getCloseReason(trade) {
172
+ return trade?.CloseReason ?? 0;
173
+ }
174
+
175
+ /**
176
+ * Check if trade was profitable.
177
+ * @param {Object} trade - Trade object
178
+ * @returns {boolean}
179
+ */
180
+ function isProfitable(trade) {
181
+ return getNetProfit(trade) > 0;
182
+ }
183
+
184
+ /**
185
+ * Calculate trade duration in days.
186
+ * @param {Object} trade - Trade object
187
+ * @returns {number}
188
+ */
189
+ function getDurationDays(trade) {
190
+ const open = getOpenDate(trade);
191
+ const close = getCloseDate(trade);
192
+
193
+ if (!open || !close) return 0;
194
+
195
+ return (close - open) / (1000 * 60 * 60 * 24);
196
+ }
197
+
198
+ /**
199
+ * Calculate price change percentage.
200
+ * @param {Object} trade - Trade object
201
+ * @returns {number}
202
+ */
203
+ function getPriceChangePct(trade) {
204
+ const open = getOpenRate(trade);
205
+ const close = getCloseRate(trade);
206
+
207
+ if (!open || open === 0) return 0;
208
+
209
+ return ((close - open) / open) * 100;
210
+ }
211
+
212
+ // =============================================================================
213
+ // FILTERING
214
+ // =============================================================================
215
+
216
+ /**
217
+ * Filter trades by date range.
218
+ * @param {Array} trades - Array of trades
219
+ * @param {Date|string} startDate - Start date
220
+ * @param {Date|string} endDate - End date
221
+ * @returns {Array}
222
+ */
223
+ function filterByDateRange(trades, startDate, endDate) {
224
+ const start = new Date(startDate);
225
+ const end = new Date(endDate);
226
+
227
+ return trades.filter(trade => {
228
+ const closeDate = getCloseDate(trade);
229
+ return closeDate && closeDate >= start && closeDate <= end;
230
+ });
231
+ }
232
+
233
+ /**
234
+ * Filter trades by instrument.
235
+ * @param {Array} trades - Array of trades
236
+ * @param {number} instrumentId - Instrument ID
237
+ * @returns {Array}
238
+ */
239
+ function filterByInstrument(trades, instrumentId) {
240
+ return trades.filter(trade => getInstrumentId(trade) === instrumentId);
241
+ }
242
+
243
+ /**
244
+ * Filter to only profitable trades.
245
+ * @param {Array} trades - Array of trades
246
+ * @returns {Array}
247
+ */
248
+ function filterProfitable(trades) {
249
+ return trades.filter(isProfitable);
250
+ }
251
+
252
+ /**
253
+ * Filter to only losing trades.
254
+ * @param {Array} trades - Array of trades
255
+ * @returns {Array}
256
+ */
257
+ function filterLosing(trades) {
258
+ return trades.filter(trade => !isProfitable(trade));
259
+ }
260
+
261
+ /**
262
+ * Filter to only long trades.
263
+ * @param {Array} trades - Array of trades
264
+ * @returns {Array}
265
+ */
266
+ function filterLong(trades) {
267
+ return trades.filter(isBuy);
268
+ }
269
+
270
+ /**
271
+ * Filter to only short trades.
272
+ * @param {Array} trades - Array of trades
273
+ * @returns {Array}
274
+ */
275
+ function filterShort(trades) {
276
+ return trades.filter(isSell);
277
+ }
278
+
279
+ /**
280
+ * Filter by leverage.
281
+ * @param {Array} trades - Array of trades
282
+ * @param {number} minLeverage - Minimum leverage
283
+ * @param {number} [maxLeverage] - Maximum leverage
284
+ * @returns {Array}
285
+ */
286
+ function filterByLeverage(trades, minLeverage, maxLeverage = Infinity) {
287
+ return trades.filter(trade => {
288
+ const lev = getLeverage(trade);
289
+ return lev >= minLeverage && lev <= maxLeverage;
290
+ });
291
+ }
292
+
293
+ // =============================================================================
294
+ // GROUPING
295
+ // =============================================================================
296
+
297
+ /**
298
+ * Group trades by instrument.
299
+ * @param {Array} trades - Array of trades
300
+ * @returns {Object} Map of instrumentId -> trades[]
301
+ */
302
+ function groupByInstrument(trades) {
303
+ const groups = {};
304
+
305
+ for (const trade of trades) {
306
+ const id = getInstrumentId(trade) || 'unknown';
307
+ if (!groups[id]) groups[id] = [];
308
+ groups[id].push(trade);
309
+ }
310
+
311
+ return groups;
312
+ }
313
+
314
+ /**
315
+ * Group trades by month (YYYY-MM).
316
+ * @param {Array} trades - Array of trades
317
+ * @returns {Object} Map of month -> trades[]
318
+ */
319
+ function groupByMonth(trades) {
320
+ const groups = {};
321
+
322
+ for (const trade of trades) {
323
+ const closeDate = getCloseDate(trade);
324
+ if (!closeDate) continue;
325
+
326
+ const month = `${closeDate.getFullYear()}-${String(closeDate.getMonth() + 1).padStart(2, '0')}`;
327
+ if (!groups[month]) groups[month] = [];
328
+ groups[month].push(trade);
329
+ }
330
+
331
+ return groups;
332
+ }
333
+
334
+ /**
335
+ * Group trades by close reason.
336
+ * @param {Array} trades - Array of trades
337
+ * @returns {Object} Map of closeReason -> trades[]
338
+ */
339
+ function groupByCloseReason(trades) {
340
+ const groups = {};
341
+
342
+ for (const trade of trades) {
343
+ const reason = getCloseReason(trade);
344
+ if (!groups[reason]) groups[reason] = [];
345
+ groups[reason].push(trade);
346
+ }
347
+
348
+ return groups;
349
+ }
350
+
351
+ // =============================================================================
352
+ // STATISTICS
353
+ // =============================================================================
354
+
355
+ /**
356
+ * Calculate comprehensive trade statistics.
357
+ * @param {Array} trades - Array of trades
358
+ * @returns {Object}
359
+ */
360
+ function calculateTradeStats(trades) {
361
+ if (!trades || trades.length === 0) {
362
+ return {
363
+ totalTrades: 0,
364
+ profitableTrades: 0,
365
+ losingTrades: 0,
366
+ winRate: 0,
367
+ avgProfit: 0,
368
+ avgWin: 0,
369
+ avgLoss: 0,
370
+ maxWin: 0,
371
+ maxLoss: 0,
372
+ profitFactor: 0,
373
+ avgDuration: 0,
374
+ longTrades: 0,
375
+ shortTrades: 0
376
+ };
377
+ }
378
+
379
+ const profitable = filterProfitable(trades);
380
+ const losing = filterLosing(trades);
381
+ const long = filterLong(trades);
382
+ const short = filterShort(trades);
383
+
384
+ const profits = trades.map(getNetProfit);
385
+ const wins = profitable.map(getNetProfit);
386
+ const losses = losing.map(getNetProfit);
387
+ const durations = trades.map(getDurationDays).filter(d => d > 0);
388
+
389
+ const totalProfit = profits.reduce((sum, p) => sum + p, 0);
390
+ const totalWins = wins.reduce((sum, p) => sum + p, 0);
391
+ const totalLosses = Math.abs(losses.reduce((sum, p) => sum + p, 0));
392
+
393
+ return {
394
+ totalTrades: trades.length,
395
+ profitableTrades: profitable.length,
396
+ losingTrades: losing.length,
397
+ winRate: trades.length > 0 ? (profitable.length / trades.length) * 100 : 0,
398
+
399
+ avgProfit: trades.length > 0 ? totalProfit / trades.length : 0,
400
+ avgWin: wins.length > 0 ? totalWins / wins.length : 0,
401
+ avgLoss: losses.length > 0 ? totalLosses / losses.length : 0,
402
+
403
+ maxWin: wins.length > 0 ? Math.max(...wins) : 0,
404
+ maxLoss: losses.length > 0 ? Math.min(...losses) : 0,
405
+
406
+ profitFactor: totalLosses > 0 ? totalWins / totalLosses : totalWins > 0 ? Infinity : 0,
407
+
408
+ avgDuration: durations.length > 0 ? durations.reduce((sum, d) => sum + d, 0) / durations.length : 0,
409
+
410
+ longTrades: long.length,
411
+ shortTrades: short.length
412
+ };
413
+ }
414
+
415
+ /**
416
+ * Calculate statistics per instrument.
417
+ * @param {Array} trades - Array of trades
418
+ * @returns {Object} Map of instrumentId -> stats
419
+ */
420
+ function calculateStatsByInstrument(trades) {
421
+ const groups = groupByInstrument(trades);
422
+ const stats = {};
423
+
424
+ for (const [instrumentId, instrumentTrades] of Object.entries(groups)) {
425
+ stats[instrumentId] = {
426
+ ...calculateTradeStats(instrumentTrades),
427
+ instrumentId
428
+ };
429
+ }
430
+
431
+ return stats;
432
+ }
433
+
434
+ /**
435
+ * Get recent trades.
436
+ * @param {Array} trades - Array of trades
437
+ * @param {number} count - Number of trades to return
438
+ * @returns {Array}
439
+ */
440
+ function getRecentTrades(trades, count = 10) {
441
+ return [...trades]
442
+ .sort((a, b) => {
443
+ const dateA = getCloseDate(a);
444
+ const dateB = getCloseDate(b);
445
+ return (dateB || 0) - (dateA || 0);
446
+ })
447
+ .slice(0, count);
448
+ }
449
+
450
+ /**
451
+ * Get best trades by profit.
452
+ * @param {Array} trades - Array of trades
453
+ * @param {number} count - Number of trades to return
454
+ * @returns {Array}
455
+ */
456
+ function getBestTrades(trades, count = 10) {
457
+ return [...trades]
458
+ .sort((a, b) => getNetProfit(b) - getNetProfit(a))
459
+ .slice(0, count);
460
+ }
461
+
462
+ /**
463
+ * Get worst trades by profit.
464
+ * @param {Array} trades - Array of trades
465
+ * @param {number} count - Number of trades to return
466
+ * @returns {Array}
467
+ */
468
+ function getWorstTrades(trades, count = 10) {
469
+ return [...trades]
470
+ .sort((a, b) => getNetProfit(a) - getNetProfit(b))
471
+ .slice(0, count);
472
+ }
473
+
474
+ /**
475
+ * Summarize trade activity.
476
+ * @param {Object} row - trade_history_snapshots row
477
+ * @returns {Object}
478
+ */
479
+ function summarize(row) {
480
+ const trades = extractTrades(row);
481
+ const stats = calculateTradeStats(trades);
482
+ const byInstrument = calculateStatsByInstrument(trades);
483
+
484
+ // Find most traded instrument
485
+ let mostTraded = null;
486
+ let mostTradedCount = 0;
487
+ for (const [id, s] of Object.entries(byInstrument)) {
488
+ if (s.totalTrades > mostTradedCount) {
489
+ mostTraded = id;
490
+ mostTradedCount = s.totalTrades;
491
+ }
492
+ }
493
+
494
+ return {
495
+ userId: getUserId(row),
496
+ ...stats,
497
+ uniqueInstruments: Object.keys(byInstrument).length,
498
+ mostTradedInstrument: mostTraded,
499
+ mostTradedCount
500
+ };
501
+ }
502
+
503
+ module.exports = {
504
+ // Data extraction
505
+ extractTrades,
506
+ getUserId,
507
+
508
+ // Trade accessors
509
+ getInstrumentId,
510
+ getNetProfit,
511
+ getOpenRate,
512
+ getCloseRate,
513
+ getOpenDate,
514
+ getCloseDate,
515
+ getLeverage,
516
+ isBuy,
517
+ isSell,
518
+ getPositionId,
519
+ getCloseReason,
520
+ isProfitable,
521
+ getDurationDays,
522
+ getPriceChangePct,
523
+
524
+ // Filtering
525
+ filterByDateRange,
526
+ filterByInstrument,
527
+ filterProfitable,
528
+ filterLosing,
529
+ filterLong,
530
+ filterShort,
531
+ filterByLeverage,
532
+
533
+ // Grouping
534
+ groupByInstrument,
535
+ groupByMonth,
536
+ groupByCloseReason,
537
+
538
+ // Statistics
539
+ calculateTradeStats,
540
+ calculateStatsByInstrument,
541
+ getRecentTrades,
542
+ getBestTrades,
543
+ getWorstTrades,
544
+ summarize
545
+ };
@@ -0,0 +1,73 @@
1
+ const admin = require('firebase-admin');
2
+ const { BigQuery } = require('@google-cloud/bigquery');
3
+
4
+ // Configuration
5
+ const PROJECT_ID = process.env.GCP_PROJECT_ID || 'stocks-12345';
6
+ const DATASET_ID = 'bulltrackers_data'; // Check your config
7
+ const TABLE_ID = 'sector_mappings';
8
+ const FIRESTORE_PATH = 'instrument_sector_mappings/sector_mappings';
9
+
10
+ async function migrate() {
11
+ console.log('🚀 Starting Sector Migration...');
12
+
13
+ // 1. Initialize Firestore
14
+ // Note: Assuming Application Default Credentials work.
15
+ // If running locally, ensure you've done: gcloud auth application-default login
16
+ admin.initializeApp({ projectId: PROJECT_ID });
17
+ const db = admin.firestore();
18
+
19
+ // 2. Fetch Firestore Data
20
+ console.log(`Reading Firestore doc: ${FIRESTORE_PATH}...`);
21
+ const docRef = db.doc(FIRESTORE_PATH);
22
+ const doc = await docRef.get();
23
+
24
+ if (!doc.exists) {
25
+ throw new Error('Firestore document not found!');
26
+ }
27
+
28
+ const data = doc.data();
29
+ const rows = Object.entries(data).map(([ticker, sector]) => ({
30
+ symbol: ticker,
31
+ sector: sector || 'N/A'
32
+ }));
33
+
34
+ console.log(`✅ Found ${rows.length} mappings.`);
35
+
36
+ // 3. Initialize BigQuery
37
+ const bigquery = new BigQuery({ projectId: PROJECT_ID });
38
+ const dataset = bigquery.dataset(DATASET_ID);
39
+ const table = dataset.table(TABLE_ID);
40
+
41
+ // 4. Create Table (if not exists)
42
+ const schema = [
43
+ { name: 'symbol', type: 'STRING', mode: 'REQUIRED' },
44
+ { name: 'sector', type: 'STRING', mode: 'NULLABLE' }
45
+ ];
46
+
47
+ const [tableExists] = await table.exists();
48
+
49
+ if (!tableExists) {
50
+ console.log(`Creating table ${TABLE_ID}...`);
51
+ await table.create({ schema });
52
+ } else {
53
+ console.log(`Table ${TABLE_ID} exists. Truncating...`);
54
+ // We truncate to ensure a clean slate (idempotent migration)
55
+ await bigquery.query(`TRUNCATE TABLE \`${PROJECT_ID}.${DATASET_ID}.${TABLE_ID}\``);
56
+ }
57
+
58
+ // 5. Insert Data
59
+ if (rows.length > 0) {
60
+ console.log('Inserting rows into BigQuery...');
61
+ // Insert in chunks to be safe
62
+ const chunkSize = 1000;
63
+ for (let i = 0; i < rows.length; i += chunkSize) {
64
+ const chunk = rows.slice(i, i + chunkSize);
65
+ await table.insert(chunk);
66
+ console.log(` Inserted rows ${i + 1} to ${i + chunk.length}`);
67
+ }
68
+ }
69
+
70
+ console.log('🎉 Migration complete!');
71
+ }
72
+
73
+ migrate().catch(console.error);