bulltrackers-module 1.0.211 → 1.0.213

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.
@@ -0,0 +1,170 @@
1
+ /**
2
+ * @fileoverview Validators Layer
3
+ * Schema validation logic adhering to schema.md production definitions.
4
+ */
5
+
6
+ const { SCHEMAS } = require('./profiling');
7
+
8
+ class Validators {
9
+
10
+ /**
11
+ * Validates a User Portfolio based on User Type.
12
+ * @param {Object} portfolio - The portfolio object.
13
+ * @param {string} userType - 'normal' or 'speculator'.
14
+ * @returns {Object} { valid: boolean, errors: string[] }
15
+ */
16
+ static validatePortfolio(portfolio, userType) {
17
+ const errors = [];
18
+ if (!portfolio || typeof portfolio !== 'object') {
19
+ return { valid: false, errors: ['Portfolio is null or invalid object'] };
20
+ }
21
+
22
+ // 1. SPECULATOR PORTFOLIO VALIDATION
23
+ if (userType === SCHEMAS.USER_TYPES.SPECULATOR) {
24
+ // Must have PublicPositions array
25
+ if (!Array.isArray(portfolio.PublicPositions)) {
26
+ errors.push('Speculator portfolio missing "PublicPositions" array');
27
+ } else {
28
+ // Validate a sample position structure (checking the first one for performance)
29
+ if (portfolio.PublicPositions.length > 0) {
30
+ const pos = portfolio.PublicPositions[0];
31
+ if (typeof pos.InstrumentID !== 'number') errors.push('Speculator Position missing numeric "InstrumentID"');
32
+ if (typeof pos.PositionID !== 'number') errors.push('Speculator Position missing numeric "PositionID"');
33
+ if (typeof pos.NetProfit !== 'number') errors.push('Speculator Position missing numeric "NetProfit"');
34
+ if (typeof pos.Leverage !== 'number') errors.push('Speculator Position missing numeric "Leverage"');
35
+ if (typeof pos.OpenRate !== 'number') errors.push('Speculator Position missing numeric "OpenRate"');
36
+ if (typeof pos.IsBuy !== 'boolean') errors.push('Speculator Position missing boolean "IsBuy"');
37
+ }
38
+ }
39
+
40
+ // Root level metrics (Speculator specific)
41
+ if (typeof portfolio.NetProfit !== 'number') errors.push('Speculator portfolio missing root "NetProfit"');
42
+ if (typeof portfolio.Invested !== 'number') errors.push('Speculator portfolio missing root "Invested"');
43
+ }
44
+
45
+ // 2. NORMAL USER PORTFOLIO VALIDATION
46
+ else {
47
+ // Must have AggregatedPositions array
48
+ if (!Array.isArray(portfolio.AggregatedPositions)) {
49
+ errors.push('Normal portfolio missing "AggregatedPositions" array');
50
+ } else {
51
+ // Validate a sample position structure
52
+ if (portfolio.AggregatedPositions.length > 0) {
53
+ const pos = portfolio.AggregatedPositions[0];
54
+ if (typeof pos.InstrumentID !== 'number') errors.push('Normal Position missing numeric "InstrumentID"');
55
+ if (typeof pos.NetProfit !== 'number') errors.push('Normal Position missing numeric "NetProfit"');
56
+ if (typeof pos.Invested !== 'number') errors.push('Normal Position missing numeric "Invested"');
57
+ if (typeof pos.Value !== 'number') errors.push('Normal Position missing numeric "Value"');
58
+ if (!pos.Direction) errors.push('Normal Position missing "Direction" string');
59
+ }
60
+ }
61
+ }
62
+
63
+ return { valid: errors.length === 0, errors };
64
+ }
65
+
66
+ /**
67
+ * Validates Trade History Data.
68
+ * @param {Object} historyDoc - The history document object.
69
+ * @returns {Object} { valid: boolean, errors: string[] }
70
+ */
71
+ static validateTradeHistory(historyDoc) {
72
+ const errors = [];
73
+ if (!historyDoc || typeof historyDoc !== 'object') {
74
+ return { valid: false, errors: ['History document is null'] };
75
+ }
76
+
77
+ if (!Array.isArray(historyDoc.PublicHistoryPositions)) {
78
+ errors.push('History missing "PublicHistoryPositions" array');
79
+ } else if (historyDoc.PublicHistoryPositions.length > 0) {
80
+ // Validate first item schema
81
+ const trade = historyDoc.PublicHistoryPositions[0];
82
+ if (typeof trade.PositionID !== 'number') errors.push('History trade missing numeric "PositionID"');
83
+ if (typeof trade.CID !== 'number') errors.push('History trade missing numeric "CID"');
84
+ if (typeof trade.InstrumentID !== 'number') errors.push('History trade missing numeric "InstrumentID"');
85
+ if (typeof trade.CloseRate !== 'number') errors.push('History trade missing numeric "CloseRate"');
86
+ if (typeof trade.CloseReason !== 'number') errors.push('History trade missing numeric "CloseReason"');
87
+ if (typeof trade.NetProfit !== 'number') errors.push('History trade missing numeric "NetProfit"');
88
+ if (!trade.OpenDateTime) errors.push('History trade missing "OpenDateTime"');
89
+ if (!trade.CloseDateTime) errors.push('History trade missing "CloseDateTime"');
90
+ }
91
+
92
+ return { valid: errors.length === 0, errors };
93
+ }
94
+
95
+ /**
96
+ * Validates Social Post Data.
97
+ * @param {Object} post - The social post object.
98
+ * @returns {Object} { valid: boolean, errors: string[] }
99
+ */
100
+ static validateSocialPost(post) {
101
+ const errors = [];
102
+ if (!post) return { valid: false, errors: ['Post is null'] };
103
+
104
+ if (!post.postOwnerId) errors.push('Post missing "postOwnerId"');
105
+ if (!post.fullText) errors.push('Post missing "fullText"');
106
+ if (!post.createdAt) errors.push('Post missing "createdAt"');
107
+ if (typeof post.likeCount !== 'number') errors.push('Post missing numeric "likeCount"');
108
+
109
+ // Sentiment Map
110
+ if (!post.sentiment || typeof post.sentiment.overallSentiment !== 'string') {
111
+ errors.push('Post missing valid "sentiment.overallSentiment"');
112
+ }
113
+
114
+ return { valid: errors.length === 0, errors };
115
+ }
116
+
117
+ /**
118
+ * Validates Insights Data (Platform Ownership).
119
+ * @param {Object} insight - A single insight item from the array.
120
+ * @returns {Object} { valid: boolean, errors: string[] }
121
+ */
122
+ static validateInsight(insight) {
123
+ const errors = [];
124
+ if (!insight) return { valid: false, errors: ['Insight object is null'] };
125
+
126
+ if (typeof insight.instrumentId !== 'number') errors.push('Insight missing numeric "instrumentId"');
127
+ if (typeof insight.total !== 'number') errors.push('Insight missing numeric "total" (Total Owners)');
128
+ if (typeof insight.percentage !== 'number') errors.push('Insight missing numeric "percentage" (Global Ownership)');
129
+ if (typeof insight.growth !== 'number') errors.push('Insight missing numeric "growth"');
130
+
131
+ // Ownership split check
132
+ if (typeof insight.buy !== 'number') errors.push('Insight missing numeric "buy" %');
133
+ if (typeof insight.sell !== 'number') errors.push('Insight missing numeric "sell" %');
134
+
135
+ return { valid: errors.length === 0, errors };
136
+ }
137
+
138
+ /**
139
+ * Validates Asset Price Shard Data.
140
+ * @param {Object} instrumentData - The price data map for a specific instrument ID.
141
+ * @returns {Object} { valid: boolean, errors: string[] }
142
+ */
143
+ static validatePriceData(instrumentData) {
144
+ const errors = [];
145
+ if (!instrumentData) return { valid: false, errors: ['Price data is null'] };
146
+
147
+ // Must have 'prices' map
148
+ if (!instrumentData.prices || typeof instrumentData.prices !== 'object') {
149
+ errors.push('Instrument price data missing "prices" map');
150
+ } else {
151
+ // Check at least one price key matches date format YYYY-MM-DD
152
+ const keys = Object.keys(instrumentData.prices);
153
+ if (keys.length > 0) {
154
+ const sampleKey = keys[0];
155
+ const sampleVal = instrumentData.prices[sampleKey];
156
+
157
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(sampleKey)) {
158
+ errors.push(`Price map key "${sampleKey}" does not match YYYY-MM-DD format`);
159
+ }
160
+ if (typeof sampleVal !== 'number') {
161
+ errors.push('Price value is not a number');
162
+ }
163
+ }
164
+ }
165
+
166
+ return { valid: errors.length === 0, errors };
167
+ }
168
+ }
169
+
170
+ module.exports = { Validators };
@@ -1,64 +1,64 @@
1
- /**
2
- * @fileoverview Schema capture utility for computation outputs
3
- * This module batches and stores pre-defined static schemas in Firestore.
4
- */
5
-
6
- /**
7
- * Batch store schemas for multiple computations.
8
- * This function now expects a fully-formed schema, not sample output.
9
- * It strictly stamps a 'lastUpdated' field to support stale-schema filtering in the API.
10
- *
11
- * @param {object} dependencies - Contains db, logger
12
- * @param {object} config - Configuration object
13
- * @param {Array} schemas - Array of {name, category, schema, metadata} objects
14
- */
15
- async function batchStoreSchemas(dependencies, config, schemas) {
16
- const { db, logger } = dependencies;
17
-
18
- if (config.captureSchemas === false) {
19
- logger.log('INFO', '[SchemaCapture] Schema capture is disabled. Skipping.');
20
- return;
21
- }
22
-
23
- const batch = db.batch();
24
- const schemaCollection = config.schemaCollection || 'computation_schemas';
25
- let validCount = 0;
26
-
27
- for (const item of schemas) {
28
- try {
29
- if (!item.schema) {
30
- logger.log('WARN', `[SchemaCapture] No schema provided for ${item.name}. Skipping.`);
31
- continue;
32
- }
33
-
34
- const docRef = db.collection(schemaCollection).doc(item.name);
35
-
36
- // Critical: Always overwrite 'lastUpdated' to now
37
- batch.set(docRef, {
38
- computationName: item.name,
39
- category: item.category,
40
- schema: item.schema,
41
- metadata: item.metadata || {},
42
- lastUpdated: new Date()
43
- }, { merge: true });
44
-
45
- validCount++;
46
-
47
- } catch (error) {
48
- logger.log('WARN', `[SchemaCapture] Failed to add schema to batch for ${item.name}`, { errorMessage: error.message });
49
- }
50
- }
51
-
52
- if (validCount > 0) {
53
- try {
54
- await batch.commit();
55
- logger.log('INFO', `[SchemaCapture] Batch stored ${validCount} computation schemas`);
56
- } catch (error) {
57
- logger.log('ERROR', '[SchemaCapture] Failed to commit schema batch', { errorMessage: error.message });
58
- }
59
- }
60
- }
61
-
62
- module.exports = {
63
- batchStoreSchemas
1
+ /**
2
+ * @fileoverview Schema capture utility for computation outputs
3
+ * This module batches and stores pre-defined static schemas in Firestore.
4
+ */
5
+
6
+ /**
7
+ * Batch store schemas for multiple computations.
8
+ * This function now expects a fully-formed schema, not sample output.
9
+ * It strictly stamps a 'lastUpdated' field to support stale-schema filtering in the API.
10
+ *
11
+ * @param {object} dependencies - Contains db, logger
12
+ * @param {object} config - Configuration object
13
+ * @param {Array} schemas - Array of {name, category, schema, metadata} objects
14
+ */
15
+ async function batchStoreSchemas(dependencies, config, schemas) {
16
+ const { db, logger } = dependencies;
17
+
18
+ if (config.captureSchemas === false) {
19
+ logger.log('INFO', '[SchemaCapture] Schema capture is disabled. Skipping.');
20
+ return;
21
+ }
22
+
23
+ const batch = db.batch();
24
+ const schemaCollection = config.schemaCollection || 'computation_schemas';
25
+ let validCount = 0;
26
+
27
+ for (const item of schemas) {
28
+ try {
29
+ if (!item.schema) {
30
+ logger.log('WARN', `[SchemaCapture] No schema provided for ${item.name}. Skipping.`);
31
+ continue;
32
+ }
33
+
34
+ const docRef = db.collection(schemaCollection).doc(item.name);
35
+
36
+ // Critical: Always overwrite 'lastUpdated' to now
37
+ batch.set(docRef, {
38
+ computationName: item.name,
39
+ category: item.category,
40
+ schema: item.schema,
41
+ metadata: item.metadata || {},
42
+ lastUpdated: new Date()
43
+ }, { merge: true });
44
+
45
+ validCount++;
46
+
47
+ } catch (error) {
48
+ logger.log('WARN', `[SchemaCapture] Failed to add schema to batch for ${item.name}`, { errorMessage: error.message });
49
+ }
50
+ }
51
+
52
+ if (validCount > 0) {
53
+ try {
54
+ await batch.commit();
55
+ logger.log('INFO', `[SchemaCapture] Batch stored ${validCount} computation schemas`);
56
+ } catch (error) {
57
+ logger.log('ERROR', '[SchemaCapture] Failed to commit schema batch', { errorMessage: error.message });
58
+ }
59
+ }
60
+ }
61
+
62
+ module.exports = {
63
+ batchStoreSchemas
64
64
  };
@@ -2,13 +2,34 @@
2
2
  * @fileoverview Computation system sub-pipes and utils.
3
3
  * REFACTORED: Now stateless and receive dependencies where needed.
4
4
  * FIXED: 'commitBatchInChunks' now respects Firestore 10MB size limit.
5
+ * NEW: Added 'generateCodeHash' for version control.
5
6
  */
6
7
 
7
8
  const { FieldValue, FieldPath } = require('@google-cloud/firestore');
9
+ const crypto = require('crypto');
8
10
 
9
11
  /** Stage 1: Normalize a calculation name to kebab-case */
10
12
  function normalizeName(name) { return name.replace(/_/g, '-'); }
11
13
 
14
+ /**
15
+ * Generates a SHA-256 hash of a code string, ignoring comments and whitespace.
16
+ * This effectively versions the logic.
17
+ * @param {string} codeString - The source code of the function/class.
18
+ * @returns {string} The hex hash.
19
+ */
20
+ function generateCodeHash(codeString) {
21
+ if (!codeString) return 'unknown';
22
+
23
+ // 1. Remove single-line comments (//...)
24
+ let clean = codeString.replace(/\/\/.*$/gm, '');
25
+ // 2. Remove multi-line comments (/*...*/)
26
+ clean = clean.replace(/\/\*[\s\S]*?\*\//g, '');
27
+ // 3. Remove all whitespace (spaces, tabs, newlines)
28
+ clean = clean.replace(/\s+/g, '');
29
+
30
+ return crypto.createHash('sha256').update(clean).digest('hex');
31
+ }
32
+
12
33
  /** * Stage 2: Commit a batch of writes in chunks
13
34
  * FIXED: Now splits batches by SIZE (9MB limit) and COUNT (450 docs)
14
35
  * to prevent "Request payload size exceeds the limit" errors.
@@ -249,4 +270,4 @@ async function getFirstDateFromPriceCollection(config, deps) {
249
270
  }
250
271
  }
251
272
 
252
- module.exports = { FieldValue, FieldPath, normalizeName, commitBatchInChunks, getExpectedDateStrings, getEarliestDataDates };
273
+ module.exports = { FieldValue, FieldPath, normalizeName, commitBatchInChunks, getExpectedDateStrings, getEarliestDataDates, generateCodeHash };
@@ -1,32 +1,20 @@
1
1
  /*
2
2
  * FILENAME: CloudFunctions/NpmWrappers/bulltrackers-module/functions/task-engine/helpers/update_helpers.js
3
- * (OPTIMIZED V4: Auto-Speculator Detection via History/Portfolio Intersection)
4
- * (OPTIMIZED V3: Removed obsolete username lookup logic)
5
- * (OPTIMIZED V2: Added "Circuit Breaker" for Proxy failures)
6
- * (REFACTORED: Concurrency set to 1, added fallback and verbose logging)
7
- * (FIXED: Improved logging clarity for Normal vs Speculator users)
8
- * (FIXED: Final log now accurately reflects failure state)
3
+ * (OPTIMIZED V5: Filter Copy Trash from History)
4
+ * FIX: Filters PublicHistoryPositions to keep only valid close reasons (0, 1, 5).
9
5
  */
10
6
 
11
7
  const { FieldValue } = require('@google-cloud/firestore');
12
8
  const crypto = require('crypto');
13
9
 
14
10
  // --- CIRCUIT BREAKER STATE ---
15
- // Persists across function invocations in the same instance.
16
- // If the Proxy fails 3 times in a row, we stop trying it to save the 5s timeout cost.
17
11
  let _consecutiveProxyFailures = 0;
18
12
  const MAX_PROXY_FAILURES = 3;
19
13
 
20
- /**
21
- * Helper to check if we should attempt the proxy
22
- */
23
14
  function shouldTryProxy() {
24
15
  return _consecutiveProxyFailures < MAX_PROXY_FAILURES;
25
16
  }
26
17
 
27
- /**
28
- * Helper to record proxy result
29
- */
30
18
  function recordProxyOutcome(success) {
31
19
  if (success) {
32
20
  _consecutiveProxyFailures = 0;
@@ -35,14 +23,9 @@ function recordProxyOutcome(success) {
35
23
  }
36
24
  }
37
25
 
38
- /**
39
- * --- NEW HELPER: Speculator Detector ---
40
- * intersections: (History: Leverage > 1) AND (Portfolio: Currently Owned)
41
- */
42
26
  function detectSpeculatorTargets(historyData, portfolioData) {
43
27
  if (!historyData?.PublicHistoryPositions || !portfolioData?.AggregatedPositions) return [];
44
28
 
45
- // 1. Identify assets that have EVER been traded with leverage > 1
46
29
  const leveragedAssets = new Set();
47
30
  for (const pos of historyData.PublicHistoryPositions) {
48
31
  if (pos.Leverage > 1 && pos.InstrumentID) {
@@ -52,7 +35,6 @@ function detectSpeculatorTargets(historyData, portfolioData) {
52
35
 
53
36
  if (leveragedAssets.size === 0) return [];
54
37
 
55
- // 2. Check if the user CURRENTLY owns any of these assets
56
38
  const targets = [];
57
39
  for (const pos of portfolioData.AggregatedPositions) {
58
40
  if (leveragedAssets.has(pos.InstrumentID)) {
@@ -63,26 +45,18 @@ function detectSpeculatorTargets(historyData, portfolioData) {
63
45
  return targets;
64
46
  }
65
47
 
66
- /**
67
- * (REFACTORED: Fully sequential, verbose logging, node-fetch fallback)
68
- */
69
48
  async function handleUpdate(task, taskId, { logger, headerManager, proxyManager, db, batchManager, pubsub }, config) {
70
49
  const { userId, instruments, instrumentId, userType } = task;
71
50
 
72
- // Normalize the loop: Speculators get specific IDs, Normal users get [undefined] to trigger one pass.
73
51
  const instrumentsToProcess = userType === 'speculator' ? (instruments || [instrumentId]) : [undefined];
74
52
  const today = new Date().toISOString().slice(0, 10);
75
53
  const portfolioBlockId = `${Math.floor(parseInt(userId) / 1000000)}M`;
76
54
  let isPrivate = false;
77
55
 
78
- // Captured data for detection logic
79
56
  let capturedHistory = null;
80
57
  let capturedPortfolio = null;
81
-
82
- // Track overall success for the final log
83
58
  let hasPortfolioErrors = false;
84
59
 
85
- // FIX 1: Better Start Log
86
60
  const scopeLog = userType === 'speculator' ? `Instruments: [${instrumentsToProcess.join(', ')}]` : 'Scope: Full Portfolio';
87
61
  logger.log('TRACE', `[handleUpdate/${userId}] Starting update task. Type: ${userType}. ${scopeLog}`);
88
62
 
@@ -99,7 +73,6 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
99
73
  logger.log('WARN', `[handleUpdate/${userId}] Could not select history header. Skipping history.`);
100
74
  } else {
101
75
 
102
- // --- REFACTOR: New Granular API Logic ---
103
76
  const d = new Date();
104
77
  d.setFullYear(d.getFullYear() - 1);
105
78
  const oneYearAgoStr = d.toISOString();
@@ -140,7 +113,18 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
140
113
 
141
114
  if (wasHistorySuccess) {
142
115
  const data = await response.json();
143
- capturedHistory = data; // Capture for later
116
+
117
+ // --- FILTER LOGIC FOR GRANULAR API ---
118
+ // 0 = Manual, 1 = Stop Loss, 5 = Take Profit.
119
+ const VALID_REASONS = [0, 1, 5];
120
+ if (data.PublicHistoryPositions && Array.isArray(data.PublicHistoryPositions)) {
121
+ const originalCount = data.PublicHistoryPositions.length;
122
+ data.PublicHistoryPositions = data.PublicHistoryPositions.filter(p => VALID_REASONS.includes(p.CloseReason));
123
+ const filteredCount = data.PublicHistoryPositions.length;
124
+ logger.log('INFO', `[handleUpdate/${userId}] History Filter: Reduced ${originalCount} -> ${filteredCount} positions.`);
125
+ }
126
+
127
+ capturedHistory = data;
144
128
  await batchManager.addToTradingHistoryBatch(userId, portfolioBlockId, today, data, userType);
145
129
  }
146
130
  }
@@ -157,7 +141,6 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
157
141
  logger.log('TRACE', `[handleUpdate/${userId}] Starting ${instrumentsToProcess.length} sequential portfolio fetches.`);
158
142
 
159
143
  for (const instId of instrumentsToProcess) {
160
- // FIX 2: Define a clear scope name for logging
161
144
  const scopeName = instId ? `Instrument ${instId}` : 'Full Portfolio';
162
145
 
163
146
  if (isPrivate) {
@@ -174,7 +157,6 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
174
157
  let wasPortfolioSuccess = false;
175
158
  let proxyUsedForPortfolio = false;
176
159
 
177
- // --- PROXY ATTEMPT ---
178
160
  if (shouldTryProxy()) {
179
161
  try {
180
162
  logger.log('TRACE', `[handleUpdate/${userId}] Attempting fetch for ${scopeName} via AppScript proxy...`);
@@ -190,7 +172,6 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
190
172
  }
191
173
  }
192
174
 
193
- // --- DIRECT FALLBACK ---
194
175
  if (!wasPortfolioSuccess) {
195
176
  try {
196
177
  response = await fetch(portfolioUrl, options);
@@ -203,35 +184,29 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
203
184
  }
204
185
  }
205
186
 
206
- // --- 4. Process Portfolio Result ---
207
187
  if (wasPortfolioSuccess) {
208
188
  const body = await response.text();
209
189
  if (body.includes("user is PRIVATE")) { isPrivate = true; logger.log('WARN', `[handleUpdate/${userId}] User is PRIVATE. Marking for removal.`); break; }
210
190
 
211
191
  try {
212
192
  const portfolioJson = JSON.parse(body);
213
- capturedPortfolio = portfolioJson; // Capture for detection
193
+ capturedPortfolio = portfolioJson;
214
194
  await batchManager.addToPortfolioBatch(userId, portfolioBlockId, today, portfolioJson, userType, instId);
215
195
  logger.log('TRACE', `[handleUpdate/${userId}] Portfolio for ${scopeName} processed successfully.`);
216
196
 
217
197
  } catch (parseError) {
218
198
  wasPortfolioSuccess = false;
219
- hasPortfolioErrors = true; // Mark error state
199
+ hasPortfolioErrors = true;
220
200
  logger.log('ERROR', `[handleUpdate/${userId}] FAILED TO PARSE JSON RESPONSE for ${scopeName}.`, { url: portfolioUrl, parseErrorMessage: parseError.message });
221
201
  }
222
202
  } else {
223
- hasPortfolioErrors = true; // Mark error state
203
+ hasPortfolioErrors = true;
224
204
  logger.log('WARN', `[handleUpdate/${userId}] Portfolio fetch FAILED for ${scopeName}.`);
225
205
  }
226
206
 
227
207
  if (proxyUsedForPortfolio) { headerManager.updatePerformance(portfolioHeader.id, wasPortfolioSuccess); }
228
208
  }
229
209
 
230
- // --- 5. SPECULATOR DETECTION & QUEUEING (NEW) ---
231
- // Only run detection if:
232
- // 1. We are processing a Normal User (userType !== 'speculator')
233
- // 2. We successfully fetched both history and portfolio
234
- // 3. We have PubSub available to queue new tasks
235
210
  if (userType !== 'speculator' && capturedHistory && capturedPortfolio && pubsub && config.PUBSUB_TOPIC_TASK_ENGINE) {
236
211
  try {
237
212
  const speculatorAssets = detectSpeculatorTargets(capturedHistory, capturedPortfolio);
@@ -245,7 +220,6 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
245
220
  instrumentId: assetId
246
221
  }));
247
222
 
248
- // Publish to Task Engine (Tasks are wrapped in a 'tasks' array payload)
249
223
  const dataBuffer = Buffer.from(JSON.stringify({ tasks: newTasks }));
250
224
  await pubsub.topic(config.PUBSUB_TOPIC_TASK_ENGINE).publishMessage({ data: dataBuffer });
251
225
  }
@@ -254,7 +228,6 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
254
228
  }
255
229
  }
256
230
 
257
- // --- 6. Handle Private Users & Timestamps ---
258
231
  if (isPrivate) {
259
232
  logger.log('WARN', `[handleUpdate/${userId}] Removing private user from updates.`);
260
233
  for (const instrumentId of instrumentsToProcess) {
@@ -268,17 +241,12 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
268
241
  return;
269
242
  }
270
243
 
271
- // If not private AND no critical errors, update timestamps
272
- // (We update timestamps even on partial failures for speculators to avoid infinite retry loops immediately,
273
- // relying on the next scheduled run, but for Normal users, a failure usually means we should retry later.
274
- // Current logic: Update timestamp to prevent immediate re-queueing.)
275
244
  for (const instrumentId of instrumentsToProcess) {
276
245
  await batchManager.updateUserTimestamp(userId, userType, instrumentId);
277
246
  }
278
247
 
279
248
  if (userType === 'speculator') { await batchManager.addSpeculatorTimestampFix(userId, String(Math.floor(userId/1e6)*1e6)); }
280
249
 
281
- // FIX 3: Honest Final Log
282
250
  if (hasPortfolioErrors) {
283
251
  logger.log('WARN', `[handleUpdate/${userId}] Update task finished with ERRORS. See logs above.`);
284
252
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.211",
3
+ "version": "1.0.213",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [