bulltrackers-module 1.0.185 → 1.0.187
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.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FIXED: computation_controller.js
|
|
3
|
-
*
|
|
3
|
+
* V4.0: Implements Batch/Sharded Execution for Price Dependencies to prevent OOM.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const { DataExtractor,
|
|
@@ -43,36 +43,54 @@ class DataLoader {
|
|
|
43
43
|
this.cache.social.set(dateStr, social);
|
|
44
44
|
return social;
|
|
45
45
|
}
|
|
46
|
+
|
|
46
47
|
/**
|
|
47
|
-
* NEW:
|
|
48
|
+
* NEW: Get references to all price shards without loading data.
|
|
49
|
+
*/
|
|
50
|
+
async getPriceShardReferences() {
|
|
51
|
+
const { db, logger } = this.deps;
|
|
52
|
+
const collection = this.config.priceCollection || 'asset_prices';
|
|
53
|
+
try {
|
|
54
|
+
const refs = await db.collection(collection).listDocuments();
|
|
55
|
+
logger.log('INFO', `[DataLoader] Found ${refs.length} price shards to process.`);
|
|
56
|
+
return refs;
|
|
57
|
+
} catch (e) {
|
|
58
|
+
logger.log('ERROR', `[DataLoader] Failed to list price shards: ${e.message}`);
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* NEW: Load a single price shard.
|
|
65
|
+
*/
|
|
66
|
+
async loadPriceShard(docRef) {
|
|
67
|
+
try {
|
|
68
|
+
const snap = await docRef.get();
|
|
69
|
+
if (!snap.exists) return {};
|
|
70
|
+
return snap.data();
|
|
71
|
+
} catch (e) {
|
|
72
|
+
console.error(`Error loading shard ${docRef.path}:`, e);
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* LEGACY: Kept for non-batched calls if ever needed, but effectively replaced by batching.
|
|
48
79
|
*/
|
|
49
80
|
async loadPrices() {
|
|
50
|
-
if (this.cache.prices) return this.cache.prices;
|
|
51
81
|
const { db, logger } = this.deps;
|
|
52
82
|
const collection = this.config.priceCollection || 'asset_prices';
|
|
53
|
-
|
|
54
|
-
logger.log('INFO', `[DataLoader] Loading all price shards from ${collection}...`);
|
|
55
|
-
|
|
83
|
+
logger.log('WARN', `[DataLoader] loadPrices() called. This is memory intensive!`);
|
|
56
84
|
try {
|
|
57
85
|
const snapshot = await db.collection(collection).get();
|
|
58
86
|
if (snapshot.empty) return { history: {} };
|
|
59
|
-
|
|
60
87
|
const historyMap = {};
|
|
61
|
-
|
|
62
88
|
snapshot.forEach(doc => {
|
|
63
89
|
const shardData = doc.data();
|
|
64
|
-
// Merge shard keys (instrumentIds) into main map
|
|
65
90
|
if (shardData) Object.assign(historyMap, shardData);
|
|
66
91
|
});
|
|
67
|
-
|
|
68
|
-
logger.log('INFO', `[DataLoader] Loaded prices for ${Object.keys(historyMap).length} instruments.`);
|
|
69
|
-
|
|
70
|
-
// Cache as an object with 'history' map to match priceExtractor expectations
|
|
71
|
-
this.cache.prices = { history: historyMap };
|
|
72
|
-
return this.cache.prices;
|
|
73
|
-
|
|
92
|
+
return { history: historyMap };
|
|
74
93
|
} catch (e) {
|
|
75
|
-
logger.log('ERROR', `[DataLoader] Failed to load prices: ${e.message}`);
|
|
76
94
|
return { history: {} };
|
|
77
95
|
}
|
|
78
96
|
}
|
|
@@ -81,21 +99,9 @@ class DataLoader {
|
|
|
81
99
|
class ContextBuilder {
|
|
82
100
|
static buildPerUserContext(options) {
|
|
83
101
|
const {
|
|
84
|
-
todayPortfolio,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
yesterdayHistory,
|
|
88
|
-
userId,
|
|
89
|
-
userType,
|
|
90
|
-
dateStr,
|
|
91
|
-
metadata,
|
|
92
|
-
mappings,
|
|
93
|
-
insights,
|
|
94
|
-
socialData,
|
|
95
|
-
computedDependencies,
|
|
96
|
-
previousComputedDependencies,
|
|
97
|
-
config,
|
|
98
|
-
deps
|
|
102
|
+
todayPortfolio, yesterdayPortfolio, todayHistory, yesterdayHistory,
|
|
103
|
+
userId, userType, dateStr, metadata, mappings, insights, socialData,
|
|
104
|
+
computedDependencies, previousComputedDependencies, config, deps
|
|
99
105
|
} = options;
|
|
100
106
|
|
|
101
107
|
return {
|
|
@@ -131,23 +137,16 @@ class ContextBuilder {
|
|
|
131
137
|
|
|
132
138
|
static buildMetaContext(options) {
|
|
133
139
|
const {
|
|
134
|
-
dateStr,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
insights,
|
|
138
|
-
socialData,
|
|
139
|
-
prices, // <--- ADDED THIS
|
|
140
|
-
computedDependencies,
|
|
141
|
-
previousComputedDependencies,
|
|
142
|
-
config,
|
|
143
|
-
deps
|
|
140
|
+
dateStr, metadata, mappings, insights, socialData,
|
|
141
|
+
prices,
|
|
142
|
+
computedDependencies, previousComputedDependencies, config, deps
|
|
144
143
|
} = options;
|
|
145
144
|
|
|
146
145
|
return {
|
|
147
146
|
date: { today: dateStr },
|
|
148
147
|
insights: { today: insights?.today, yesterday: insights?.yesterday },
|
|
149
148
|
social: { today: socialData?.today, yesterday: socialData?.yesterday },
|
|
150
|
-
prices: prices || {},
|
|
149
|
+
prices: prices || {},
|
|
151
150
|
mappings: mappings || {},
|
|
152
151
|
math: {
|
|
153
152
|
extract: DataExtractor,
|
|
@@ -207,27 +206,70 @@ class ComputationExecutor {
|
|
|
207
206
|
}
|
|
208
207
|
}
|
|
209
208
|
|
|
209
|
+
/**
|
|
210
|
+
* REFACTORED: Batched execution for Price dependencies
|
|
211
|
+
*/
|
|
210
212
|
async executeOncePerDay(calcInstance, metadata, dateStr, computedDeps, prevDeps) {
|
|
211
213
|
const mappings = await this.loader.loadMappings();
|
|
214
|
+
const { logger } = this.deps;
|
|
212
215
|
|
|
213
|
-
// Load standard dependencies
|
|
214
216
|
const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await this.loader.loadInsights(dateStr) } : null;
|
|
215
217
|
const social = metadata.rootDataDependencies?.includes('social') ? { today: await this.loader.loadSocial(dateStr) } : null;
|
|
216
218
|
|
|
217
|
-
//
|
|
218
|
-
let prices = null;
|
|
219
|
+
// CHECK: Does this calculation require price history?
|
|
219
220
|
if (metadata.rootDataDependencies?.includes('price')) {
|
|
220
|
-
|
|
221
|
-
|
|
221
|
+
logger.log('INFO', `[Executor] Running Batched/Sharded Execution for ${metadata.name}`);
|
|
222
|
+
|
|
223
|
+
// 1. Get Shard References (Low Memory)
|
|
224
|
+
const shardRefs = await this.loader.getPriceShardReferences();
|
|
225
|
+
|
|
226
|
+
if (shardRefs.length === 0) {
|
|
227
|
+
logger.log('WARN', '[Executor] No price shards found.');
|
|
228
|
+
return {};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 2. Iterate Shards (One at a time in memory)
|
|
232
|
+
let processedCount = 0;
|
|
233
|
+
for (const ref of shardRefs) {
|
|
234
|
+
// Load ONE shard
|
|
235
|
+
const shardData = await this.loader.loadPriceShard(ref);
|
|
236
|
+
|
|
237
|
+
// Construct a "Partial Context" containing only this shard's prices
|
|
238
|
+
const partialContext = ContextBuilder.buildMetaContext({
|
|
239
|
+
dateStr, metadata, mappings, insights, socialData: social,
|
|
240
|
+
prices: { history: shardData }, // Inject partial history
|
|
241
|
+
computedDependencies: computedDeps,
|
|
242
|
+
previousComputedDependencies: prevDeps,
|
|
243
|
+
config: this.config, deps: this.deps
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Process the chunk
|
|
247
|
+
// IMPORTANT: The calc instance must ACCUMULATE results, not overwrite.
|
|
248
|
+
await calcInstance.process(partialContext);
|
|
249
|
+
|
|
250
|
+
// Force dereference for GC
|
|
251
|
+
partialContext.prices = null;
|
|
252
|
+
processedCount++;
|
|
253
|
+
|
|
254
|
+
if (processedCount % 10 === 0) {
|
|
255
|
+
if (global.gc) { global.gc(); } // Optional: Hint GC if exposed
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
logger.log('INFO', `[Executor] Finished Batched Execution for ${metadata.name} (${processedCount} shards).`);
|
|
260
|
+
return calcInstance.getResult ? await calcInstance.getResult() : {};
|
|
222
261
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
262
|
+
} else {
|
|
263
|
+
// Standard execution for non-price calculations
|
|
264
|
+
const context = ContextBuilder.buildMetaContext({
|
|
265
|
+
dateStr, metadata, mappings, insights, socialData: social,
|
|
266
|
+
prices: {},
|
|
267
|
+
computedDependencies: computedDeps,
|
|
268
|
+
previousComputedDependencies: prevDeps,
|
|
269
|
+
config: this.config, deps: this.deps
|
|
270
|
+
});
|
|
271
|
+
return await calcInstance.process(context);
|
|
272
|
+
}
|
|
231
273
|
}
|
|
232
274
|
}
|
|
233
275
|
|
|
@@ -25,9 +25,18 @@ async function runComputationPass(config, dependencies, computationManifest) {
|
|
|
25
25
|
|
|
26
26
|
logger.log('INFO', `🚀 Starting PASS ${passToRun} (Targeting /computation_status/{YYYY-MM-DD})...`);
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
// Hardcoded earliest dates
|
|
29
|
+
const earliestDates = {
|
|
30
|
+
portfolio: new Date('2025-09-25T00:00:00Z'),
|
|
31
|
+
history: new Date('2025-11-05T00:00:00Z'),
|
|
32
|
+
social: new Date('2025-10-30T00:00:00Z'),
|
|
33
|
+
insights: new Date('2025-08-26T00:00:00Z'),
|
|
34
|
+
price: new Date('2025-08-01T00:00:00Z') // A few weeks before insights (earliest other data)
|
|
35
|
+
|
|
36
|
+
};
|
|
29
37
|
earliestDates.absoluteEarliest = Object.values(earliestDates).reduce((a,b) => a < b ? a : b);
|
|
30
38
|
|
|
39
|
+
|
|
31
40
|
const passes = groupByPass(computationManifest);
|
|
32
41
|
const calcsInThisPass = passes[passToRun] || [];
|
|
33
42
|
|
|
@@ -37,7 +37,7 @@ function checkRootDependencies(calcManifest, rootDataStatus) {
|
|
|
37
37
|
* Checks for the availability of all required root data for a specific date.
|
|
38
38
|
*/
|
|
39
39
|
async function checkRootDataAvailability(dateStr, config, dependencies, earliestDates) {
|
|
40
|
-
const { logger
|
|
40
|
+
const { logger } = dependencies;
|
|
41
41
|
const dateToProcess = new Date(dateStr + 'T00:00:00Z');
|
|
42
42
|
let portfolioRefs = [], historyRefs = [];
|
|
43
43
|
let hasPortfolio = false, hasInsights = false, hasSocial = false, hasHistory = false, hasPrices = false;
|
|
@@ -50,8 +50,8 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
|
|
|
50
50
|
if (dateToProcess >= earliestDates.social) tasks.push(loadDailySocialPostInsights(config, dependencies, dateStr).then(r => { socialData = r; hasSocial = !!r; }));
|
|
51
51
|
if (dateToProcess >= earliestDates.history) tasks.push(getHistoryPartRefs(config, dependencies, dateStr).then(r => { historyRefs = r; hasHistory = !!r.length; }));
|
|
52
52
|
|
|
53
|
-
// NEW: Check if price data exists
|
|
54
|
-
if (dateToProcess >=
|
|
53
|
+
// NEW: Check if price data exists - simple validation
|
|
54
|
+
if (dateToProcess >= earliestDates.price) {
|
|
55
55
|
tasks.push(checkPriceDataAvailability(config, dependencies).then(r => { hasPrices = r; }));
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -74,14 +74,14 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/**
|
|
77
|
-
* NEW HELPER:
|
|
77
|
+
* NEW HELPER: Simple check if price collection has any data
|
|
78
78
|
*/
|
|
79
79
|
async function checkPriceDataAvailability(config, dependencies) {
|
|
80
80
|
const { db, logger } = dependencies;
|
|
81
81
|
const collection = config.priceCollection || 'asset_prices';
|
|
82
82
|
|
|
83
83
|
try {
|
|
84
|
-
//
|
|
84
|
+
// Just check if the collection has at least one document
|
|
85
85
|
const snapshot = await db.collection(collection).limit(1).get();
|
|
86
86
|
|
|
87
87
|
if (snapshot.empty) {
|
|
@@ -89,16 +89,6 @@ async function checkPriceDataAvailability(config, dependencies) {
|
|
|
89
89
|
return false;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
// Check if the first shard actually has price data
|
|
93
|
-
const firstDoc = snapshot.docs[0];
|
|
94
|
-
const data = firstDoc.data();
|
|
95
|
-
|
|
96
|
-
if (!data || Object.keys(data).length === 0) {
|
|
97
|
-
logger.log('WARN', `[checkPriceData] Price shard exists but is empty`);
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
logger.log('TRACE', `[checkPriceData] Price data available in ${collection}`);
|
|
102
92
|
return true;
|
|
103
93
|
|
|
104
94
|
} catch (e) {
|