bulltrackers-module 1.0.183 → 1.0.185
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.
- package/functions/computation-system/controllers/computation_controller.js +9 -18
- package/functions/computation-system/helpers/computation_pass_runner.js +2 -3
- package/functions/computation-system/helpers/orchestration_helpers.js +78 -23
- package/functions/computation-system/utils/utils.js +114 -14
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FIXED: computation_controller.js
|
|
3
|
-
* V3.
|
|
3
|
+
* V3.4: Adds Price Loading & Context Injection.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const { DataExtractor,
|
|
@@ -55,34 +55,25 @@ class DataLoader {
|
|
|
55
55
|
|
|
56
56
|
try {
|
|
57
57
|
const snapshot = await db.collection(collection).get();
|
|
58
|
-
if (snapshot.empty) return { history:
|
|
58
|
+
if (snapshot.empty) return { history: {} };
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
// Structure expected by calculation: Array of { instrumentId, prices: {...} }
|
|
62
|
-
const allPrices = [];
|
|
60
|
+
const historyMap = {};
|
|
63
61
|
|
|
64
62
|
snapshot.forEach(doc => {
|
|
65
63
|
const shardData = doc.data();
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
if (data && data.prices) {
|
|
69
|
-
allPrices.push({
|
|
70
|
-
instrumentId: instId,
|
|
71
|
-
...data
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
}
|
|
64
|
+
// Merge shard keys (instrumentIds) into main map
|
|
65
|
+
if (shardData) Object.assign(historyMap, shardData);
|
|
75
66
|
});
|
|
76
67
|
|
|
77
|
-
logger.log('INFO', `[DataLoader] Loaded prices for ${
|
|
68
|
+
logger.log('INFO', `[DataLoader] Loaded prices for ${Object.keys(historyMap).length} instruments.`);
|
|
78
69
|
|
|
79
|
-
// Cache as an object with 'history'
|
|
80
|
-
this.cache.prices = { history:
|
|
70
|
+
// Cache as an object with 'history' map to match priceExtractor expectations
|
|
71
|
+
this.cache.prices = { history: historyMap };
|
|
81
72
|
return this.cache.prices;
|
|
82
73
|
|
|
83
74
|
} catch (e) {
|
|
84
75
|
logger.log('ERROR', `[DataLoader] Failed to load prices: ${e.message}`);
|
|
85
|
-
return { history:
|
|
76
|
+
return { history: {} };
|
|
86
77
|
}
|
|
87
78
|
}
|
|
88
79
|
}
|
|
@@ -13,7 +13,7 @@ const {
|
|
|
13
13
|
checkRootDependencies
|
|
14
14
|
} = require('./orchestration_helpers.js');
|
|
15
15
|
|
|
16
|
-
const { getExpectedDateStrings, normalizeName } = require('../utils/utils.js');
|
|
16
|
+
const { getExpectedDateStrings, normalizeName, getEarliestDataDates } = require('../utils/utils.js');
|
|
17
17
|
|
|
18
18
|
const PARALLEL_BATCH_SIZE = 7;
|
|
19
19
|
|
|
@@ -25,8 +25,7 @@ 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
|
-
//
|
|
29
|
-
const earliestDates = { portfolio: new Date('2025-09-25T00:00:00Z'), history: new Date('2025-11-05T00:00:00Z'), social: new Date('2025-10-30T00:00:00Z'), insights: new Date('2025-08-26T00:00:00Z') };
|
|
28
|
+
const earliestDates = await getEarliestDataDates(config, dependencies); // Now not hardcoded.
|
|
30
29
|
earliestDates.absoluteEarliest = Object.values(earliestDates).reduce((a,b) => a < b ? a : b);
|
|
31
30
|
|
|
32
31
|
const passes = groupByPass(computationManifest);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FILENAME: bulltrackers-module/functions/computation-system/helpers/orchestration_helpers.js
|
|
3
|
-
* FIXED:
|
|
3
|
+
* FIXED: Explicit Logging + Honest Status Updates
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const { ComputationController } = require('../controllers/computation_controller');
|
|
@@ -28,8 +28,7 @@ function checkRootDependencies(calcManifest, rootDataStatus) {
|
|
|
28
28
|
else if (dep === 'insights' && !rootDataStatus.hasInsights) missing.push('insights');
|
|
29
29
|
else if (dep === 'social' && !rootDataStatus.hasSocial) missing.push('social');
|
|
30
30
|
else if (dep === 'history' && !rootDataStatus.hasHistory) missing.push('history');
|
|
31
|
-
|
|
32
|
-
// but usually prices are treated as auxiliary. If you want to block on prices, add it here.
|
|
31
|
+
else if (dep === 'price' && !rootDataStatus.hasPrices) missing.push('price'); // NEW
|
|
33
32
|
}
|
|
34
33
|
return { canRun: missing.length === 0, missing };
|
|
35
34
|
}
|
|
@@ -38,27 +37,34 @@ function checkRootDependencies(calcManifest, rootDataStatus) {
|
|
|
38
37
|
* Checks for the availability of all required root data for a specific date.
|
|
39
38
|
*/
|
|
40
39
|
async function checkRootDataAvailability(dateStr, config, dependencies, earliestDates) {
|
|
41
|
-
const { logger }
|
|
40
|
+
const { logger, db } = dependencies;
|
|
42
41
|
const dateToProcess = new Date(dateStr + 'T00:00:00Z');
|
|
43
|
-
let portfolioRefs
|
|
44
|
-
let hasPortfolio
|
|
42
|
+
let portfolioRefs = [], historyRefs = [];
|
|
43
|
+
let hasPortfolio = false, hasInsights = false, hasSocial = false, hasHistory = false, hasPrices = false;
|
|
44
|
+
let insightsData = null, socialData = null;
|
|
45
45
|
|
|
46
46
|
try {
|
|
47
47
|
const tasks = [];
|
|
48
|
-
if (dateToProcess >= earliestDates.portfolio)
|
|
49
|
-
if (dateToProcess >= earliestDates.insights)
|
|
50
|
-
if (dateToProcess >= earliestDates.social)
|
|
51
|
-
if (dateToProcess >= earliestDates.history)
|
|
48
|
+
if (dateToProcess >= earliestDates.portfolio) tasks.push(getPortfolioPartRefs(config, dependencies, dateStr).then(r => { portfolioRefs = r; hasPortfolio = !!r.length; }));
|
|
49
|
+
if (dateToProcess >= earliestDates.insights) tasks.push(loadDailyInsights(config, dependencies, dateStr).then(r => { insightsData = r; hasInsights = !!r; }));
|
|
50
|
+
if (dateToProcess >= earliestDates.social) tasks.push(loadDailySocialPostInsights(config, dependencies, dateStr).then(r => { socialData = r; hasSocial = !!r; }));
|
|
51
|
+
if (dateToProcess >= earliestDates.history) tasks.push(getHistoryPartRefs(config, dependencies, dateStr).then(r => { historyRefs = r; hasHistory = !!r.length; }));
|
|
52
|
+
|
|
53
|
+
// NEW: Check if price data exists (proper validation)
|
|
54
|
+
if (dateToProcess >= (earliestDates.price || earliestDates.absoluteEarliest)) {
|
|
55
|
+
tasks.push(checkPriceDataAvailability(config, dependencies).then(r => { hasPrices = r; }));
|
|
56
|
+
}
|
|
52
57
|
|
|
53
58
|
await Promise.all(tasks);
|
|
54
59
|
|
|
55
|
-
|
|
56
|
-
if (!(hasPortfolio || hasInsights || hasSocial || hasHistory)) return null;
|
|
60
|
+
if (!(hasPortfolio || hasInsights || hasSocial || hasHistory || hasPrices)) return null;
|
|
57
61
|
|
|
58
62
|
return {
|
|
59
|
-
portfolioRefs,
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
portfolioRefs,
|
|
64
|
+
historyRefs,
|
|
65
|
+
todayInsights: insightsData,
|
|
66
|
+
todaySocialPostInsights: socialData,
|
|
67
|
+
status: { hasPortfolio, hasInsights, hasSocial, hasHistory, hasPrices }
|
|
62
68
|
};
|
|
63
69
|
|
|
64
70
|
} catch (err) {
|
|
@@ -67,6 +73,40 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
|
|
|
67
73
|
}
|
|
68
74
|
}
|
|
69
75
|
|
|
76
|
+
/**
|
|
77
|
+
* NEW HELPER: Check if price data collection has any data
|
|
78
|
+
*/
|
|
79
|
+
async function checkPriceDataAvailability(config, dependencies) {
|
|
80
|
+
const { db, logger } = dependencies;
|
|
81
|
+
const collection = config.priceCollection || 'asset_prices';
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
// Check if the collection has at least one shard document
|
|
85
|
+
const snapshot = await db.collection(collection).limit(1).get();
|
|
86
|
+
|
|
87
|
+
if (snapshot.empty) {
|
|
88
|
+
logger.log('WARN', `[checkPriceData] No price shards found in ${collection}`);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
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
|
+
return true;
|
|
103
|
+
|
|
104
|
+
} catch (e) {
|
|
105
|
+
logger.log('ERROR', `[checkPriceData] Failed to check price availability: ${e.message}`);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
70
110
|
async function fetchComputationStatus(dateStr, config, { db }) {
|
|
71
111
|
const collection = config.computationStatusCollection || 'computation_status';
|
|
72
112
|
const docRef = db.collection(collection).doc(dateStr);
|
|
@@ -205,6 +245,9 @@ async function runStandardComputationPass(date, calcs, passName, config, deps, r
|
|
|
205
245
|
const inst = new c.class();
|
|
206
246
|
inst.manifest = c;
|
|
207
247
|
state[normalizeName(c.name)] = inst;
|
|
248
|
+
|
|
249
|
+
// LOG: Explicitly say what calculation is being processed (Initialized)
|
|
250
|
+
logger.log('INFO', `${c.name} calculation running for ${dStr}`);
|
|
208
251
|
}
|
|
209
252
|
catch(e) {
|
|
210
253
|
logger.log('WARN', `Failed to init ${c.name}`);
|
|
@@ -223,6 +266,9 @@ async function runMetaComputationPass(date, calcs, passName, config, deps, fetch
|
|
|
223
266
|
|
|
224
267
|
for (const mCalc of calcs) {
|
|
225
268
|
try {
|
|
269
|
+
// LOG: Explicitly say what calculation is being processed
|
|
270
|
+
deps.logger.log('INFO', `${mCalc.name} calculation running for ${dStr}`);
|
|
271
|
+
|
|
226
272
|
const inst = new mCalc.class();
|
|
227
273
|
inst.manifest = mCalc;
|
|
228
274
|
await controller.executor.executeOncePerDay(inst, mCalc, dStr, fetchedDeps, previousFetchedDeps);
|
|
@@ -234,8 +280,8 @@ async function runMetaComputationPass(date, calcs, passName, config, deps, fetch
|
|
|
234
280
|
}
|
|
235
281
|
|
|
236
282
|
/**
|
|
237
|
-
* ---
|
|
238
|
-
*
|
|
283
|
+
* --- UPDATED: commitResults ---
|
|
284
|
+
* Includes Explicit Result Logging and Honest Status Reporting.
|
|
239
285
|
*/
|
|
240
286
|
async function commitResults(stateObj, dStr, passName, config, deps, skipStatusWrite = false) {
|
|
241
287
|
const writes = [], schemas = [], sharded = {};
|
|
@@ -245,10 +291,15 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
245
291
|
const calc = stateObj[name];
|
|
246
292
|
try {
|
|
247
293
|
const result = await calc.getResult();
|
|
248
|
-
|
|
294
|
+
|
|
295
|
+
// If null/undefined, log as Failed/Unknown immediately
|
|
296
|
+
if (!result) {
|
|
297
|
+
deps.logger.log('INFO', `${name} calculation for ${dStr} ran, result : Failed (Empty Result)`);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
249
300
|
|
|
250
301
|
const standardRes = {};
|
|
251
|
-
let hasData = false;
|
|
302
|
+
let hasData = false;
|
|
252
303
|
|
|
253
304
|
for (const key in result) {
|
|
254
305
|
if (key.startsWith('sharded_')) {
|
|
@@ -284,15 +335,19 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
284
335
|
});
|
|
285
336
|
}
|
|
286
337
|
|
|
287
|
-
//
|
|
338
|
+
// --- EXPLICIT LOGGING & STATUS UPDATE ---
|
|
288
339
|
if (hasData) {
|
|
289
340
|
successUpdates[name] = true;
|
|
341
|
+
deps.logger.log('INFO', `${name} calculation for ${dStr} ran, result : Succeeded`);
|
|
290
342
|
} else {
|
|
291
|
-
//
|
|
292
|
-
|
|
343
|
+
// It ran without error, but produced no content (e.g. no data met criteria)
|
|
344
|
+
deps.logger.log('INFO', `${name} calculation for ${dStr} ran, result : Unknown (No Data Written)`);
|
|
293
345
|
}
|
|
294
346
|
|
|
295
|
-
} catch (e) {
|
|
347
|
+
} catch (e) {
|
|
348
|
+
deps.logger.log('ERROR', `Commit failed ${name}: ${e.message}`);
|
|
349
|
+
deps.logger.log('INFO', `${name} calculation for ${dStr} ran, result : Failed (Exception)`);
|
|
350
|
+
}
|
|
296
351
|
}
|
|
297
352
|
|
|
298
353
|
if (schemas.length) batchStoreSchemas(deps, config, schemas).catch(()=>{});
|
|
@@ -75,25 +75,125 @@ async function getFirstDateFromCollection(config, deps, collectionName) {
|
|
|
75
75
|
async function getEarliestDataDates(config, deps) {
|
|
76
76
|
const { logger } = deps;
|
|
77
77
|
logger.log('INFO', 'Querying for earliest date from ALL source data collections...');
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
|
|
79
|
+
const [
|
|
80
|
+
investorDate,
|
|
81
|
+
speculatorDate,
|
|
82
|
+
investorHistoryDate,
|
|
83
|
+
speculatorHistoryDate,
|
|
84
|
+
insightsDate,
|
|
85
|
+
socialDate,
|
|
86
|
+
priceDate // NEW
|
|
87
|
+
] = await Promise.all([
|
|
88
|
+
getFirstDateFromCollection(config, deps, config.normalUserPortfolioCollection),
|
|
89
|
+
getFirstDateFromCollection(config, deps, config.speculatorPortfolioCollection),
|
|
90
|
+
getFirstDateFromCollection(config, deps, config.normalUserHistoryCollection),
|
|
91
|
+
getFirstDateFromCollection(config, deps, config.speculatorHistoryCollection),
|
|
83
92
|
getFirstDateFromSimpleCollection(config, deps, config.insightsCollectionName),
|
|
84
|
-
getFirstDateFromSimpleCollection(config, deps, config.socialInsightsCollectionName)
|
|
93
|
+
getFirstDateFromSimpleCollection(config, deps, config.socialInsightsCollectionName),
|
|
94
|
+
getFirstDateFromPriceCollection(config, deps) // NEW
|
|
85
95
|
]);
|
|
86
96
|
|
|
87
|
-
const getMinDate
|
|
97
|
+
const getMinDate = (...dates) => {
|
|
98
|
+
const validDates = dates.filter(Boolean);
|
|
99
|
+
if (validDates.length === 0) return null;
|
|
100
|
+
return new Date(Math.min(...validDates));
|
|
101
|
+
};
|
|
102
|
+
|
|
88
103
|
const earliestPortfolioDate = getMinDate(investorDate, speculatorDate);
|
|
89
|
-
const earliestHistoryDate
|
|
90
|
-
const earliestInsightsDate
|
|
91
|
-
const earliestSocialDate
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
104
|
+
const earliestHistoryDate = getMinDate(investorHistoryDate, speculatorHistoryDate);
|
|
105
|
+
const earliestInsightsDate = getMinDate(insightsDate);
|
|
106
|
+
const earliestSocialDate = getMinDate(socialDate);
|
|
107
|
+
const earliestPriceDate = getMinDate(priceDate); // NEW
|
|
108
|
+
const absoluteEarliest = getMinDate(
|
|
109
|
+
earliestPortfolioDate,
|
|
110
|
+
earliestHistoryDate,
|
|
111
|
+
earliestInsightsDate,
|
|
112
|
+
earliestSocialDate,
|
|
113
|
+
earliestPriceDate // NEW
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const fallbackDate = new Date(config.earliestComputationDate + 'T00:00:00Z' || '2023-01-01T00:00:00Z');
|
|
117
|
+
|
|
118
|
+
const result = {
|
|
119
|
+
portfolio: earliestPortfolioDate || new Date('2999-12-31'),
|
|
120
|
+
history: earliestHistoryDate || new Date('2999-12-31'),
|
|
121
|
+
insights: earliestInsightsDate || new Date('2999-12-31'),
|
|
122
|
+
social: earliestSocialDate || new Date('2999-12-31'),
|
|
123
|
+
price: earliestPriceDate || new Date('2999-12-31'), // NEW
|
|
124
|
+
absoluteEarliest: absoluteEarliest || fallbackDate
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
logger.log('INFO', 'Earliest data availability map built:', {
|
|
128
|
+
portfolio: result.portfolio.toISOString().slice(0, 10),
|
|
129
|
+
history: result.history.toISOString().slice(0, 10),
|
|
130
|
+
insights: result.insights.toISOString().slice(0, 10),
|
|
131
|
+
social: result.social.toISOString().slice(0, 10),
|
|
132
|
+
price: result.price.toISOString().slice(0, 10), // NEW
|
|
133
|
+
absoluteEarliest: result.absoluteEarliest.toISOString().slice(0, 10)
|
|
134
|
+
});
|
|
135
|
+
|
|
96
136
|
return result;
|
|
97
137
|
}
|
|
98
138
|
|
|
139
|
+
/**
|
|
140
|
+
* NEW HELPER: Get the earliest date from price collection
|
|
141
|
+
* Price data is sharded differently - each shard contains instrumentId -> {prices: {date: price}}
|
|
142
|
+
*/
|
|
143
|
+
async function getFirstDateFromPriceCollection(config, deps) {
|
|
144
|
+
const { db, logger, calculationUtils } = deps;
|
|
145
|
+
const { withRetry } = calculationUtils;
|
|
146
|
+
const collection = config.priceCollection || 'asset_prices';
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
logger.log('TRACE', `[getFirstDateFromPriceCollection] Querying ${collection}...`);
|
|
150
|
+
|
|
151
|
+
// Get all shards (limit to first few for performance)
|
|
152
|
+
const snapshot = await withRetry(
|
|
153
|
+
() => db.collection(collection).limit(10).get(),
|
|
154
|
+
`GetPriceShards(${collection})`
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (snapshot.empty) {
|
|
158
|
+
logger.log('WARN', `No price shards found in ${collection}`);
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let earliestDate = null;
|
|
163
|
+
|
|
164
|
+
// Iterate through shards to find the earliest date across all instruments
|
|
165
|
+
snapshot.forEach(doc => {
|
|
166
|
+
const shardData = doc.data();
|
|
167
|
+
|
|
168
|
+
// Each shard has structure: { instrumentId: { ticker, prices: { "YYYY-MM-DD": price } } }
|
|
169
|
+
for (const instrumentId in shardData) {
|
|
170
|
+
const instrumentData = shardData[instrumentId];
|
|
171
|
+
if (!instrumentData.prices) continue;
|
|
172
|
+
|
|
173
|
+
// Get all dates for this instrument
|
|
174
|
+
const dates = Object.keys(instrumentData.prices)
|
|
175
|
+
.filter(d => /^\d{4}-\d{2}-\d{2}$/.test(d))
|
|
176
|
+
.sort();
|
|
177
|
+
|
|
178
|
+
if (dates.length > 0) {
|
|
179
|
+
const firstDate = new Date(dates[0] + 'T00:00:00Z');
|
|
180
|
+
if (!earliestDate || firstDate < earliestDate) {
|
|
181
|
+
earliestDate = firstDate;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (earliestDate) {
|
|
188
|
+
logger.log('TRACE', `[getFirstDateFromPriceCollection] Earliest price date: ${earliestDate.toISOString().slice(0, 10)}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return earliestDate;
|
|
192
|
+
|
|
193
|
+
} catch (e) {
|
|
194
|
+
logger.log('ERROR', `Failed to get earliest price date from ${collection}`, { errorMessage: e.message });
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
99
199
|
module.exports = { FieldValue, FieldPath, normalizeName, commitBatchInChunks, getExpectedDateStrings, getEarliestDataDates };
|