bulltrackers-module 1.0.212 → 1.0.214

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,23 +1,19 @@
1
1
  /**
2
2
  * @fileoverview
3
- * Dynamic Manifest Builder (v5 - Smart Hashing with Safe Mode)
3
+ * Dynamic Manifest Builder (v6 - Merkle Tree Dependency Hashing)
4
4
  *
5
- * This script builds the computation manifest and generates a "Smart Hash"
6
- * for each calculation.
7
- *
8
- * KEY FEATURE:
9
- * It performs static analysis on the calculation code to detect which
10
- * specific math layers it utilizes. It then combines the calculation's own
11
- * code hash with the current hashes of those specific layers.
12
- * * SAFE MODE:
13
- * If a calculation uses no detectable layer keywords, it defaults to
14
- * depending on ALL layers to prevent staleness.
5
+ * KEY FEATURES:
6
+ * 1. Smart Layer Hashing: Detects used layers (Math, Extractors) to avoid stale helper code.
7
+ * 2. Cascading Invalidation (Merkle Hashing):
8
+ * The final hash of a computation is derived from:
9
+ * [Own Code] + [Layer States] + [Hashes of all Dependencies]
10
+ * * This guarantees that if Calculation A is updated, Calculation B (which depends on A)
11
+ * will automatically generate a new hash, forcing the system to re-run it.
15
12
  */
16
13
 
17
14
  const { generateCodeHash } = require('../utils/utils');
18
15
 
19
16
  // 1. Import Layers directly to generate their "State Hashes"
20
- // We import them individually to hash them as distinct domains.
21
17
  const MathematicsLayer = require('../layers/mathematics');
22
18
  const ExtractorsLayer = require('../layers/extractors');
23
19
  const ProfilingLayer = require('../layers/profiling');
@@ -27,10 +23,6 @@ const ValidatorsLayer = require('../layers/validators');
27
23
  * 1. Layer Hash Generation
28
24
  * -------------------------------------------------- */
29
25
 
30
- /**
31
- * Generates a single hash representing the entire state of a layer module.
32
- * Combines all exported classes/functions/objects into one deterministic string.
33
- */
34
26
  function generateLayerHash(layerExports, layerName) {
35
27
  const keys = Object.keys(layerExports).sort(); // Sort for determinism
36
28
  let combinedSource = `LAYER:${layerName}`;
@@ -57,21 +49,20 @@ const LAYER_HASHES = {
57
49
  };
58
50
 
59
51
  // Map code patterns to Layer dependencies
60
- // If a calculation's code contains these strings, it depends on that layer.
61
52
  const LAYER_TRIGGERS = {
62
53
  'mathematics': [
63
54
  'math.compute', 'MathPrimitives',
64
- 'math.signals', 'SignalPrimitives',
55
+ 'math.signals', 'SignalPrimitives', 'signals.',
65
56
  'math.aggregate', 'Aggregators',
66
- 'math.timeseries', 'TimeSeries',
67
- 'math.distribution', 'DistributionAnalytics',
57
+ 'math.timeseries', 'TimeSeries', 'timeSeries.',
58
+ 'math.distribution', 'DistributionAnalytics', 'distribution.',
68
59
  'math.financial', 'FinancialEngineering'
69
60
  ],
70
61
  'extractors': [
71
62
  'math.extract', 'DataExtractor',
72
63
  'math.history', 'HistoryExtractor',
73
64
  'math.prices', 'priceExtractor',
74
- 'math.insights', 'InsightsExtractor',
65
+ 'math.insights', 'InsightsExtractor', 'insights.',
75
66
  'math.tradeSeries', 'TradeSeriesBuilder'
76
67
  ],
77
68
  'profiling': [
@@ -121,18 +112,6 @@ function suggestClosest(name, candidates, n = 3) {
121
112
  return scores.slice(0, n).map(s => s[0]);
122
113
  }
123
114
 
124
- function findCycles(manifestMap, adjacencyList) {
125
- const visited = new Set(), stack = new Set(), cycles = [];
126
- const dfs = (node, path) => {
127
- if (stack.has(node)) { const idx = path.indexOf(node); cycles.push([...path.slice(idx), node]); return; }
128
- if (visited.has(node)) return;
129
- visited.add(node); stack.add(node);
130
- for (const nb of adjacencyList.get(node) || []) dfs(nb, [...path, nb]);
131
- stack.delete(node); };
132
- for (const name of manifestMap.keys()) dfs(name, [name]);
133
- return cycles;
134
- }
135
-
136
115
  function getDependencySet(endpoints, adjacencyList) {
137
116
  const required = new Set(endpoints);
138
117
  const queue = [...endpoints];
@@ -146,24 +125,21 @@ function getDependencySet(endpoints, adjacencyList) {
146
125
  * -------------------------------------------------- */
147
126
 
148
127
  function buildManifest(productLinesToRun = [], calculations) {
149
- log.divider('Building Dynamic Manifest (Smart Hashing)');
128
+ log.divider('Building Dynamic Manifest (Merkle Hashing)');
150
129
  log.info(`Target Product Lines: [${productLinesToRun.join(', ')}]`);
151
- log.info(`Layer Hashes Initialized: ${Object.keys(LAYER_HASHES).join(', ')}`);
152
-
130
+
153
131
  const manifestMap = new Map();
154
132
  const adjacency = new Map();
155
133
  const reverseAdjacency = new Map();
156
134
  const inDegree = new Map();
157
135
  let hasFatalError = false;
158
136
 
159
- /* ---------------- 1. Load All Calculations ---------------- */
137
+ /* ---------------- 1. Load All Calculations (Phase 1: Intrinsic Hash) ---------------- */
160
138
  log.step('Loading and validating all calculation classes…');
161
- const allCalculationClasses = new Map();
162
139
 
163
140
  function processCalc(Class, name, folderName) {
164
141
  if (!Class || typeof Class !== 'function') return;
165
142
  const normalizedName = normalizeName(name);
166
- allCalculationClasses.set(normalizedName, Class);
167
143
 
168
144
  if (typeof Class.getMetadata !== 'function') { log.fatal(`Calculation "${normalizedName}" is missing static getMetadata().`); hasFatalError = true; return; }
169
145
  if (typeof Class.getDependencies !== 'function') { log.fatal(`Calculation "${normalizedName}" is missing static getDependencies().`); hasFatalError = true;return; }
@@ -171,46 +147,34 @@ function buildManifest(productLinesToRun = [], calculations) {
171
147
  const metadata = Class.getMetadata();
172
148
  const dependencies = Class.getDependencies().map(normalizeName);
173
149
 
174
- if (metadata.isHistorical === true && !Class.toString().includes('yesterday')) {
175
- log.warn(`Calculation "${normalizedName}" marked 'isHistorical' but no 'yesterday' reference found.`);
150
+ const codeStr = Class.toString();
151
+ if (metadata.isHistorical === true && !codeStr.includes('yesterday') && !codeStr.includes('previousComputed')) {
152
+ log.warn(`Calculation "${normalizedName}" marked 'isHistorical' but no 'previousComputed' state reference found.`);
176
153
  }
177
154
 
178
- let finalCategory = folderName;
179
- if (folderName === 'core') {
180
- if (metadata.category) finalCategory = metadata.category;
181
- } else {
182
- finalCategory = folderName;
183
- }
155
+ let finalCategory = folderName === 'core' && metadata.category ? metadata.category : folderName;
184
156
 
185
- // --- SMART HASH GENERATION ---
186
- const classCode = Class.toString();
187
- let compositeHashString = generateCodeHash(classCode); // Start with own code
157
+ // --- PHASE 1: INTRINSIC HASH (Code + Layers) ---
158
+ // We do NOT include dependencies yet.
159
+ let compositeHashString = generateCodeHash(codeStr);
188
160
  const usedLayers = [];
189
161
 
190
- // 1. Check for specific layer usage
162
+ // Check for specific layer usage
191
163
  for (const [layerName, triggers] of Object.entries(LAYER_TRIGGERS)) {
192
- // If code contains any trigger for this layer
193
- if (triggers.some(trigger => classCode.includes(trigger))) {
194
- compositeHashString += LAYER_HASHES[layerName]; // Append Layer Hash
164
+ if (triggers.some(trigger => codeStr.includes(trigger))) {
165
+ compositeHashString += LAYER_HASHES[layerName];
195
166
  usedLayers.push(layerName);
196
167
  }
197
168
  }
198
169
 
199
- // 2. SAFE MODE FALLBACK
200
- // If we found NO specific triggers (e.g. legacy code or unique structure),
201
- // assume it might use anything. Depend on ALL layers to be safe.
170
+ // Safe Mode Fallback
202
171
  let isSafeMode = false;
203
172
  if (usedLayers.length === 0) {
204
173
  isSafeMode = true;
205
174
  Object.values(LAYER_HASHES).forEach(h => compositeHashString += h);
206
175
  }
207
176
 
208
- // Generate final composite hash
209
- const finalHash = generateCodeHash(compositeHashString);
210
-
211
- if (isSafeMode) {
212
- log.warn(`[Hash] ${normalizedName}: No specific dependencies detected. Enforcing SAFE MODE (Linked to ALL Layers).`);
213
- }
177
+ const baseHash = generateCodeHash(compositeHashString);
214
178
 
215
179
  const manifestEntry = {
216
180
  name: normalizedName,
@@ -223,7 +187,7 @@ function buildManifest(productLinesToRun = [], calculations) {
223
187
  userType: metadata.userType,
224
188
  dependencies: dependencies,
225
189
  pass: 0,
226
- hash: finalHash, // The Smart Hash
190
+ hash: baseHash, // Intrinsic Hash (Updated later to include deps)
227
191
  debugUsedLayers: isSafeMode ? ['ALL (Safe Mode)'] : usedLayers
228
192
  };
229
193
 
@@ -237,7 +201,6 @@ function buildManifest(productLinesToRun = [], calculations) {
237
201
  throw new Error('Manifest build failed: Invalid calculations object.');
238
202
  }
239
203
 
240
- // Iterate over folders (folderName becomes the default category)
241
204
  for (const folderName in calculations) {
242
205
  if (folderName === 'legacy') continue;
243
206
  const group = calculations[folderName];
@@ -308,6 +271,31 @@ function buildManifest(productLinesToRun = [], calculations) {
308
271
  if (sortedManifest.length !== filteredManifestMap.size) {
309
272
  throw new Error('Circular dependency detected. Manifest build failed.'); }
310
273
 
274
+ /* ---------------- 5. Phase 2: Cascading Dependency Hashing ---------------- */
275
+ // Now that we have a topological order (Dependencies come BEFORE Consumers),
276
+ // we can update hashes sequentially.
277
+ log.step('Computing Cascading Merkle Hashes...');
278
+
279
+ for (const entry of sortedManifest) {
280
+ // Start with the intrinsic hash (Code + Layers)
281
+ let dependencySignature = entry.hash;
282
+
283
+ // Append the hashes of all dependencies
284
+ // Since we are iterating in topo order, dependencies are guaranteed to be processed/updated already.
285
+ if (entry.dependencies && entry.dependencies.length > 0) {
286
+ const depHashes = entry.dependencies.map(depName => {
287
+ const depEntry = filteredManifestMap.get(depName);
288
+ if (!depEntry) return ''; // Should not happen given validation
289
+ return depEntry.hash;
290
+ }).join('|'); // Use separator to prevent collisions
291
+
292
+ dependencySignature += `|DEPS:${depHashes}`;
293
+ }
294
+
295
+ // Generate the Final Smart Hash
296
+ entry.hash = generateCodeHash(dependencySignature);
297
+ }
298
+
311
299
  log.success(`Total passes required: ${maxPass}`);
312
300
  return sortedManifest;
313
301
  }
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * FILENAME: bulltrackers-module/functions/computation-system/helpers/computation_pass_runner.js
3
- * FIXED: 'runDateComputation' now executes ALL calculation types (Standard, Meta, AND Price).
4
- * UPDATED: Uses Code Hash for smart versioning and invalidation.
3
+ * FIXED: 'storedStatus.substring' crash and 'missing dependency' log clarity.
5
4
  */
6
5
 
7
6
  const {
@@ -20,10 +19,6 @@ const { getExpectedDateStrings, normalizeName } = require('../utils/utils.js');
20
19
 
21
20
  const PARALLEL_BATCH_SIZE = 7;
22
21
 
23
- /**
24
- * LEGACY / MANUAL RUNNER
25
- * (Kept for backward compatibility if you run the old HTTP endpoint directly)
26
- */
27
22
  async function runComputationPass(config, dependencies, computationManifest) {
28
23
  const { logger } = dependencies;
29
24
  const passToRun = String(config.COMPUTATION_PASS_TO_RUN);
@@ -31,7 +26,6 @@ async function runComputationPass(config, dependencies, computationManifest) {
31
26
 
32
27
  logger.log('INFO', `🚀 Starting PASS ${passToRun} (Legacy Mode)...`);
33
28
 
34
- // Hardcoded earliest dates
35
29
  const earliestDates = {
36
30
  portfolio: new Date('2025-09-25T00:00:00Z'),
37
31
  history: new Date('2025-11-05T00:00:00Z'),
@@ -51,13 +45,12 @@ async function runComputationPass(config, dependencies, computationManifest) {
51
45
  const endDateUTC = new Date(Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate() - 1));
52
46
  const allExpectedDates = getExpectedDateStrings(passEarliestDate, endDateUTC);
53
47
 
54
- // Legacy Batch Optimization for Price (Only used in legacy loop)
55
48
  const priceBatchCalcs = calcsInThisPass.filter(c => c.type === 'meta' && c.rootDataDependencies?.includes('price'));
56
49
  const standardAndOtherMetaCalcs = calcsInThisPass.filter(c => !priceBatchCalcs.includes(c));
57
50
 
58
51
  if (priceBatchCalcs.length > 0) {
59
52
  try {
60
- await runBatchPriceComputation(config, dependencies, allExpectedDates, priceBatchCalcs); // Simplified for legacy
53
+ await runBatchPriceComputation(config, dependencies, allExpectedDates, priceBatchCalcs);
61
54
  } catch (e) { logger.log('ERROR', 'Legacy Batch Price failed', e); }
62
55
  }
63
56
 
@@ -69,61 +62,56 @@ async function runComputationPass(config, dependencies, computationManifest) {
69
62
  }
70
63
  }
71
64
 
72
- /**
73
- * UPDATED: Isolated function to run computations for a single date.
74
- * Uses Code Hash to determine if a re-run is necessary.
75
- */
76
65
  async function runDateComputation(dateStr, passToRun, calcsInThisPass, config, dependencies, computationManifest) {
77
66
  const { logger } = dependencies;
78
67
  const dateToProcess = new Date(dateStr + 'T00:00:00Z');
79
68
 
80
- // 1. Fetch Status for THIS specific date only
81
69
  const dailyStatus = await fetchComputationStatus(dateStr, config, dependencies);
82
70
 
83
- // Helper: Check status using HASH comparison
84
- const shouldRun = (calc) => {
71
+ // Filter AND Log reason for skipping
72
+ const calcsToAttempt = [];
73
+
74
+ for (const calc of calcsInThisPass) {
85
75
  const cName = normalizeName(calc.name);
86
76
  const storedStatus = dailyStatus[cName];
87
77
  const currentHash = calc.hash;
88
78
 
89
- // If dependency logic is needed, check dependencies are 'complete'
90
- // 'Complete' means truthy (hash or true)
79
+ // 1. Dependency Check
91
80
  if (calc.dependencies && calc.dependencies.length > 0) {
92
- const missing = calc.dependencies.filter(depName => {
93
- const depStatus = dailyStatus[normalizeName(depName)];
94
- return !depStatus; // Run if dependency is missing entirely
95
- });
96
- if (missing.length > 0) return false; // Wait for dependency
81
+ const missing = calc.dependencies.filter(depName => !dailyStatus[normalizeName(depName)]);
82
+ if (missing.length > 0) {
83
+ // Too noisy to log every skip, but useful for debugging if needed.
84
+ // Only logging if it's NOT a bulk skip.
85
+ // logger.log('TRACE', `[Skip] ${cName} missing deps: ${missing.join(', ')}`);
86
+ continue;
87
+ }
97
88
  }
98
89
 
99
- // Logic A: No previous run
90
+ // 2. Logic A: No previous run
100
91
  if (!storedStatus) {
101
92
  logger.log('INFO', `[Versioning] ${cName}: New run needed (No prior status).`);
102
- return true;
93
+ calcsToAttempt.push(calc);
94
+ continue;
103
95
  }
104
96
 
105
- // Logic B: Hash Mismatch (Code Changed or Layer Changed)
106
- if (currentHash && storedStatus !== currentHash) {
107
- logger.log('INFO', `[Versioning] ${cName}: Code/Layer Changed. Re-running. (Old: ${storedStatus.substring(0,6)}... New: ${currentHash.substring(0,6)}...)`);
108
- return true;
97
+ // 3. Logic B: Hash Mismatch
98
+ // FIX: Ensure storedStatus is a string before calling substring
99
+ if (typeof storedStatus === 'string' && currentHash && storedStatus !== currentHash) {
100
+ logger.log('INFO', `[Versioning] ${cName}: Code Changed. (Old: ${storedStatus.substring(0,6)}... New: ${currentHash.substring(0,6)}...)`);
101
+ calcsToAttempt.push(calc);
102
+ continue;
109
103
  }
110
104
 
111
- // Logic C: Legacy boolean check (Stored=true) vs New Hash
112
- // If stored is strictly boolean true, but we have a hash, we upgrade (re-run) to stamp the hash.
105
+ // 4. Logic C: Upgrade Legacy Boolean -> Hash
113
106
  if (storedStatus === true && currentHash) {
114
- logger.log('INFO', `[Versioning] ${cName}: Upgrading legacy status to Hash. Re-running.`);
115
- return true;
107
+ logger.log('INFO', `[Versioning] ${cName}: Upgrading legacy status to Hash.`);
108
+ calcsToAttempt.push(calc);
109
+ continue;
116
110
  }
117
-
118
- return false; // Skip
119
- };
120
-
121
- // --- FIX: Run ALL calc types (Standard, Meta, Price) ---
122
- const calcsToAttempt = calcsInThisPass.filter(shouldRun);
111
+ }
123
112
 
124
113
  if (!calcsToAttempt.length) return null;
125
114
 
126
- // 2. Check Root Data Availability
127
115
  const earliestDates = {
128
116
  portfolio: new Date('2025-09-25T00:00:00Z'),
129
117
  history: new Date('2025-11-05T00:00:00Z'),
@@ -138,14 +126,11 @@ async function runDateComputation(dateStr, passToRun, calcsInThisPass, config, d
138
126
  return null;
139
127
  }
140
128
 
141
- // 3. Filter again based on Root Data availability
142
129
  const runnableCalcs = calcsToAttempt.filter(c => checkRootDependencies(c, rootData.status).canRun);
143
130
 
144
131
  if (!runnableCalcs.length) return null;
145
132
 
146
- // Split into Standard (Streaming) and Meta (Once-Per-Day/Price)
147
133
  const standardToRun = runnableCalcs.filter(c => c.type === 'standard');
148
- // Note: Meta includes Price calcs in this flow
149
134
  const metaToRun = runnableCalcs.filter(c => c.type === 'meta');
150
135
 
151
136
  logger.log('INFO', `[DateRunner] Running ${dateStr}: ${standardToRun.length} std, ${metaToRun.length} meta`);
@@ -155,7 +140,6 @@ async function runDateComputation(dateStr, passToRun, calcsInThisPass, config, d
155
140
  try {
156
141
  const calcsRunning = [...standardToRun, ...metaToRun];
157
142
 
158
- // Fetch dependencies (results from this day or yesterday)
159
143
  const existingResults = await fetchExistingResults(dateStr, calcsRunning, computationManifest, config, dependencies, false);
160
144
  const prevDate = new Date(dateToProcess); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
161
145
  const prevDateStr = prevDate.toISOString().slice(0, 10);
@@ -166,14 +150,13 @@ async function runDateComputation(dateStr, passToRun, calcsInThisPass, config, d
166
150
  Object.assign(dateUpdates, updates);
167
151
  }
168
152
  if (metaToRun.length) {
169
- // runMetaComputationPass uses the Controller, which handles Price Sharding logic internally for single dates.
170
153
  const updates = await runMetaComputationPass(dateToProcess, metaToRun, `Pass ${passToRun} (Meta)`, config, dependencies, existingResults, previousResults, rootData, false);
171
154
  Object.assign(dateUpdates, updates);
172
155
  }
173
156
  } catch (err) {
174
157
  logger.log('ERROR', `[DateRunner] FAILED Pass ${passToRun} for ${dateStr}`, { errorMessage: err.message });
175
158
  [...standardToRun, ...metaToRun].forEach(c => dateUpdates[normalizeName(c.name)] = false);
176
- throw err; // Re-throw to trigger Pub/Sub retry
159
+ throw err;
177
160
  }
178
161
 
179
162
  if (Object.keys(dateUpdates).length > 0) {
@@ -1,45 +1,23 @@
1
1
  /**
2
2
  * @fileoverview Extractors Layer
3
- * Core access methods to raw data (Portfolio, History, Prices, Insights).
3
+ * Core access methods to raw data.
4
+ * FIX: HistoryExtractor handles both Legacy and Modern (Granular) schemas.
4
5
  */
5
6
 
6
7
  const { SCHEMAS } = require('./profiling');
7
8
 
8
9
  class TradeSeriesBuilder {
9
- /**
10
- * Converts raw trade history into a time-series of Strategy Returns.
11
- * Assumes a "Flat Bet" model (equal sizing) because absolute position size is not available in history.
12
- * This creates a normalized performance curve for the user's *decisions*.
13
- * @param {Array} historyTrades - PublicHistoryPositions array.
14
- * @returns {Array<number>} Array of NetProfit% values sorted by close date.
15
- */
16
10
  static buildReturnSeries(historyTrades) {
17
11
  if (!historyTrades || !Array.isArray(historyTrades)) return [];
18
-
19
- // 1. Filter valid closed trades
20
12
  const closedTrades = historyTrades.filter(t => t.CloseDateTime && typeof t.NetProfit === 'number');
21
-
22
- // 2. Sort by Close Date (Ascending)
23
13
  closedTrades.sort((a, b) => new Date(a.CloseDateTime) - new Date(b.CloseDateTime));
24
-
25
- // 3. Extract the PnL sequence
26
14
  return closedTrades.map(t => t.NetProfit);
27
15
  }
28
16
 
29
- /**
30
- * Builds a cumulative equity curve (starting at 100) based on compounding trade returns.
31
- * Useful for visualising the trajectory of the strategy.
32
- */
33
17
  static buildCumulativeCurve(returnSeries, startValue = 100) {
34
18
  const curve = [startValue];
35
19
  let current = startValue;
36
-
37
20
  for (const ret of returnSeries) {
38
- // Apply return (e.g. 5% profit -> * 1.05)
39
- // Note: NetProfit in eToro history is usually percentage (e.g. 5.4954).
40
- // We treat this as the return on *that specific position*.
41
- // In a flat-bet model, we assume that position was X% of the portfolio.
42
- // Simplified: We just accumulate the "points" captured.
43
21
  current = current * (1 + (ret / 100));
44
22
  curve.push(current);
45
23
  }
@@ -48,177 +26,84 @@ class TradeSeriesBuilder {
48
26
  }
49
27
 
50
28
  class DataExtractor {
51
- // ========================================================================
52
- // 1. COLLECTION ACCESSORS
53
- // ========================================================================
54
-
55
- /**
56
- * Extract positions array based on User Type.
57
- * - Normal: Uses 'AggregatedPositions' (Grouped by Asset + Direction)
58
- * - Speculator: Uses 'PublicPositions' (Individual Trades)
59
- */
60
29
  static getPositions(portfolio, userType) {
61
- if (!portfolio) return []; // Handle empty portfolio
62
-
30
+ if (!portfolio) return [];
63
31
  if (userType === SCHEMAS.USER_TYPES.SPECULATOR) {
64
- return portfolio.PublicPositions || []; // SPECULATOR SCHEMA
32
+ return portfolio.PublicPositions || [];
65
33
  }
66
-
67
- // Default to Normal User Schema
68
34
  return portfolio.AggregatedPositions || [];
69
35
  }
70
36
 
71
- // ========================================================================
72
- // 2. IDENTITY & KEYS
73
- // ========================================================================
74
-
75
- /**
76
- * Extract standardized Instrument ID.
77
- */
78
37
  static getInstrumentId(position) {
79
- if (!position) return null; // Handle empty position data
80
- // Handle string or number variations safely
38
+ if (!position) return null;
81
39
  return position.InstrumentID || position.instrumentId || null;
82
40
  }
83
41
 
84
- /**
85
- * Extract a unique Identifier for the position.
86
- * - Speculator: Uses 'PositionID'.
87
- * - Normal: Generates Composite Key (InstrumentID_Direction) since they lack unique Trade IDs.
88
- */
89
42
  static getPositionId(position) {
90
- if (!position) return null; // Handle empty position data
91
-
92
- // 1. Try Explicit ID (Speculators)
43
+ if (!position) return null;
93
44
  if (position.PositionID) return String(position.PositionID);
94
45
  if (position.PositionId) return String(position.PositionId);
95
-
96
- // 2. Fallback to Composite Key (Normal Users)
97
46
  const instId = this.getInstrumentId(position);
98
47
  const dir = this.getDirection(position);
99
48
  if (instId) return `${instId}_${dir}`;
100
-
101
49
  return null;
102
50
  }
103
51
 
104
- // ========================================================================
105
- // 3. FINANCIAL METRICS (WEIGHTS & P&L)
106
- // ========================================================================
107
-
108
- /**
109
- * Extract Net Profit %.
110
- * Schema: 'NetProfit' is the percentage profit relative to invested capital.
111
- */
112
52
  static getNetProfit(position) {
113
53
  return position ? (position.NetProfit || 0) : 0;
114
54
  }
115
55
 
116
- /**
117
- * Extract Position Weight (Allocation %).
118
- * Schema:
119
- * - Normal: 'Invested' is % of initial capital.
120
- * - Speculator: 'Invested' (or 'Amount' in some contexts) is % of initial capital.
121
- */
122
- static getPositionWeight(position, userType) { // Agnostic on user type, unused.
56
+ static getPositionWeight(position, userType) {
123
57
  if (!position) return 0;
124
-
125
- // Both schemas use 'Invested' to represent the allocation percentage.
126
- // Speculators might optionally have 'Amount', we prioritize 'Invested' for consistency.
127
58
  return position.Invested || position.Amount || 0;
128
59
  }
129
60
 
130
- /**
131
- * Extract Current Equity Value %.
132
- * Schema: 'Value' is the current value as a % of total portfolio equity.
133
- */
134
61
  static getPositionValuePct(position) {
135
62
  return position ? (position.Value || 0) : 0;
136
63
  }
137
64
 
138
- /**
139
- * --- NEW PRIMITIVE ---
140
- * Derives the approximate Entry Price of a position based on Current Price and Net Profit %.
141
- * Formula: Entry = Current / (1 + (NetProfit / 100))
142
- * @param {number} currentPrice - The current market price of the asset.
143
- * @param {number} netProfitPct - The Net Profit percentage (e.g., -20.5).
144
- * @returns {number} Estimated Entry Price.
145
- */
146
65
  static deriveEntryPrice(currentPrice, netProfitPct) {
147
66
  if (!currentPrice || currentPrice <= 0) return 0;
148
- // Avoid division by zero if P&L is -100% (unlikely but possible in crypto/options)
149
- if (netProfitPct <= -100) return Number.MAX_SAFE_INTEGER; // Effectively infinite entry price (lost everything)
67
+ if (netProfitPct <= -100) return Number.MAX_SAFE_INTEGER;
150
68
  return currentPrice / (1 + (netProfitPct / 100.0));
151
69
  }
152
70
 
153
- // ========================================================================
154
- // 4. PORTFOLIO LEVEL SUMMARY
155
- // ========================================================================
156
-
157
- /**
158
- * Calculate/Extract Daily Portfolio P&L %.
159
- */
160
71
  static getPortfolioDailyPnl(portfolio, userType) {
161
72
  if (!portfolio) return 0;
162
-
163
- // 1. Speculator (Explicit 'NetProfit' field on root)
164
73
  if (userType === SCHEMAS.USER_TYPES.SPECULATOR) {
165
74
  return portfolio.NetProfit || 0;
166
75
  }
167
-
168
- // 2. Normal (Aggregated Calculation)
169
76
  if (portfolio.AggregatedPositionsByInstrumentTypeID) {
170
77
  return portfolio.AggregatedPositionsByInstrumentTypeID.reduce((sum, agg) => {
171
78
  return sum + ((agg.Value || 0) - (agg.Invested || 0));
172
79
  }, 0);
173
80
  }
174
-
175
81
  return 0;
176
82
  }
177
83
 
178
- // ========================================================================
179
- // 5. TRADE DETAILS (SPECULATOR SPECIFIC)
180
- // ========================================================================
181
-
182
84
  static getDirection(position) {
183
85
  if (!position) return "Buy";
184
86
  if (position.Direction) return position.Direction;
185
87
  if (typeof position.IsBuy === 'boolean') return position.IsBuy ? "Buy" : "Sell";
186
- return "Buy"; // Default
187
- }
188
-
189
- static getLeverage(position) {
190
- return position ? (position.Leverage || 1) : 1; // Default 1 IF NOT FOUND
191
- }
192
-
193
- static getOpenRate(position) {
194
- return position ? (position.OpenRate || 0) : 0; // Default 0 IF NOT FOUND
195
- }
196
-
197
- static getCurrentRate(position) {
198
- return position ? (position.CurrentRate || 0) : 0; // Default 0 IF NOT FOUND
88
+ return "Buy";
199
89
  }
200
90
 
91
+ static getLeverage(position) { return position ? (position.Leverage || 1) : 1; }
92
+ static getOpenRate(position) { return position ? (position.OpenRate || 0) : 0; }
93
+ static getCurrentRate(position) { return position ? (position.CurrentRate || 0) : 0; }
201
94
  static getStopLossRate(position) {
202
95
  const rate = position ? (position.StopLossRate || 0) : 0;
203
- if (rate > 0 && rate <= 0.01) return 0; // Normalizes bug value to 0
96
+ if (rate > 0 && rate <= 0.01) return 0;
204
97
  if (rate < 0) return 0;
205
98
  return rate;
206
99
  }
207
-
208
100
  static getTakeProfitRate(position) {
209
101
  const rate = position ? (position.TakeProfitRate || 0) : 0;
210
- if (rate > 0 && rate <= 0.01) return 0; // Normalizes bug value to 0
102
+ if (rate > 0 && rate <= 0.01) return 0;
211
103
  return rate;
212
104
  }
213
-
214
- static getHasTSL(position) {
215
- return position ? (position.HasTrailingStopLoss === true) : false; // Default false IF NOT FOUND
216
- }
217
-
218
- static getOpenDateTime(position) {
219
- if (!position || !position.OpenDateTime) return null;
220
- return new Date(position.OpenDateTime);
221
- }
105
+ static getHasTSL(position) { return position ? (position.HasTrailingStopLoss === true) : false; }
106
+ static getOpenDateTime(position) { return (!position || !position.OpenDateTime) ? null : new Date(position.OpenDateTime); }
222
107
  }
223
108
 
224
109
  class priceExtractor {
@@ -235,7 +120,6 @@ class priceExtractor {
235
120
  }
236
121
 
237
122
  if (!assetData || !assetData.prices) return [];
238
-
239
123
  const priceMap = assetData.prices;
240
124
  const sortedDates = Object.keys(priceMap).sort((a, b) => a.localeCompare(b));
241
125
 
@@ -247,14 +131,11 @@ class priceExtractor {
247
131
 
248
132
  static getAllHistories(pricesContext) {
249
133
  if (!pricesContext || !pricesContext.history) return new Map();
250
-
251
134
  const results = new Map();
252
135
  for (const [id, data] of Object.entries(pricesContext.history)) {
253
136
  const ticker = data.ticker || id;
254
137
  const history = this.getHistory(pricesContext, id);
255
- if (history.length > 0) {
256
- results.set(ticker, history);
257
- }
138
+ if (history.length > 0) results.set(ticker, history);
258
139
  }
259
140
  return results;
260
141
  }
@@ -266,38 +147,45 @@ class HistoryExtractor {
266
147
  }
267
148
 
268
149
  static getTradedAssets(historyDoc) {
269
- const trades = historyDoc?.PublicHistoryPositions || [];
270
- if (!trades.length) return [];
271
-
272
- const assetsMap = new Map();
273
-
274
- for (const t of trades) {
275
- const instId = t.InstrumentID;
276
- if (!instId) continue;
277
-
278
- if (!assetsMap.has(instId)) {
279
- assetsMap.set(instId, {
280
- instrumentId: instId,
281
- totalDuration: 0,
282
- count: 0
283
- });
284
- }
150
+ // 1. Try Modern Granular Data (Derive Assets from Trades)
151
+ if (historyDoc?.PublicHistoryPositions?.length) {
152
+ const trades = historyDoc.PublicHistoryPositions;
153
+ const assetsMap = new Map();
285
154
 
286
- const asset = assetsMap.get(instId);
287
- const open = new Date(t.OpenDateTime);
288
- const close = new Date(t.CloseDateTime);
289
- const durationMins = (close - open) / 60000;
290
-
291
- if (durationMins > 0) {
292
- asset.totalDuration += durationMins;
293
- asset.count++;
155
+ for (const t of trades) {
156
+ const instId = t.InstrumentID;
157
+ if (!instId) continue;
158
+
159
+ if (!assetsMap.has(instId)) {
160
+ assetsMap.set(instId, {
161
+ instrumentId: instId,
162
+ totalDuration: 0,
163
+ count: 0
164
+ });
165
+ }
166
+
167
+ const asset = assetsMap.get(instId);
168
+ const open = new Date(t.OpenDateTime);
169
+ const close = new Date(t.CloseDateTime);
170
+ const durationMins = (close - open) / 60000;
171
+
172
+ if (durationMins > 0) {
173
+ asset.totalDuration += durationMins;
174
+ asset.count++;
175
+ }
294
176
  }
177
+ return Array.from(assetsMap.values()).map(a => ({
178
+ instrumentId: a.instrumentId,
179
+ avgHoldingTimeInMinutes: a.count > 0 ? (a.totalDuration / a.count) : 0
180
+ }));
295
181
  }
296
-
297
- return Array.from(assetsMap.values()).map(a => ({
298
- instrumentId: a.instrumentId,
299
- avgHoldingTimeInMinutes: a.count > 0 ? (a.totalDuration / a.count) : 0
300
- }));
182
+
183
+ // 2. Fallback to Legacy 'assets' array
184
+ if (historyDoc?.assets && Array.isArray(historyDoc.assets)) {
185
+ return historyDoc.assets;
186
+ }
187
+
188
+ return [];
301
189
  }
302
190
 
303
191
  static getInstrumentId(asset) {
@@ -309,79 +197,63 @@ class HistoryExtractor {
309
197
  }
310
198
 
311
199
  static getSummary(historyDoc) {
312
- const trades = historyDoc?.PublicHistoryPositions || [];
313
- if (!trades.length) return null;
314
-
315
- let totalTrades = trades.length;
316
- let wins = 0;
317
- let totalProf = 0;
318
- let totalLoss = 0;
319
- let profCount = 0;
320
- let lossCount = 0;
321
- let totalDur = 0;
322
-
323
- for (const t of trades) {
324
- if (t.NetProfit > 0) {
325
- wins++;
326
- totalProf += t.NetProfit;
327
- profCount++;
328
- } else if (t.NetProfit < 0) {
329
- totalLoss += t.NetProfit;
330
- lossCount++;
200
+ // 1. Try Modern Granular Data (Derive Summary)
201
+ if (historyDoc?.PublicHistoryPositions?.length) {
202
+ const trades = historyDoc.PublicHistoryPositions;
203
+ let totalTrades = trades.length;
204
+ let wins = 0;
205
+ let totalProf = 0;
206
+ let totalLoss = 0;
207
+ let profCount = 0;
208
+ let lossCount = 0;
209
+ let totalDur = 0;
210
+
211
+ for (const t of trades) {
212
+ if (t.NetProfit > 0) {
213
+ wins++;
214
+ totalProf += t.NetProfit;
215
+ profCount++;
216
+ } else if (t.NetProfit < 0) {
217
+ totalLoss += t.NetProfit;
218
+ lossCount++;
219
+ }
220
+ const open = new Date(t.OpenDateTime);
221
+ const close = new Date(t.CloseDateTime);
222
+ totalDur += (close - open) / 60000;
331
223
  }
332
-
333
- const open = new Date(t.OpenDateTime);
334
- const close = new Date(t.CloseDateTime);
335
- totalDur += (close - open) / 60000;
224
+
225
+ return {
226
+ totalTrades: totalTrades,
227
+ winRatio: totalTrades > 0 ? (wins / totalTrades) * 100 : 0,
228
+ avgProfitPct: profCount > 0 ? totalProf / profCount : 0,
229
+ avgLossPct: lossCount > 0 ? totalLoss / lossCount : 0,
230
+ avgHoldingTimeInMinutes: totalTrades > 0 ? totalDur / totalTrades : 0
231
+ };
336
232
  }
337
233
 
338
- return {
339
- totalTrades: totalTrades,
340
- winRatio: totalTrades > 0 ? (wins / totalTrades) * 100 : 0,
341
- avgProfitPct: profCount > 0 ? totalProf / profCount : 0,
342
- avgLossPct: lossCount > 0 ? totalLoss / lossCount : 0,
343
- avgHoldingTimeInMinutes: totalTrades > 0 ? totalDur / totalTrades : 0
344
- };
234
+ // 2. Fallback to Legacy 'all' object
235
+ if (historyDoc?.all) {
236
+ return historyDoc.all;
237
+ }
238
+
239
+ return null;
345
240
  }
346
241
  }
347
242
 
348
243
  class InsightsExtractor {
349
- /**
350
- * Extracts the raw array of insight objects from the context.
351
- * Checks for standard context injection paths.
352
- */
353
244
  static getInsights(context) {
354
- // Support multiple potential injection paths depending on controller version
355
245
  return context.insights || context.daily_instrument_insights || [];
356
246
  }
357
247
 
358
- /**
359
- * returns the specific insight object for a given instrument ID.
360
- */
361
248
  static getInsightForInstrument(insights, instrumentId) {
362
249
  if (!insights || !Array.isArray(insights)) return null;
363
250
  return insights.find(i => i.instrumentId === instrumentId) || null;
364
251
  }
365
252
 
366
- // --- Standard Metrics ---
367
-
368
- static getTotalOwners(insight) {
369
- return insight ? (insight.total || 0) : 0;
370
- }
371
-
372
- static getLongPercent(insight) {
373
- return insight ? (insight.buy || 0) : 0;
374
- }
375
-
376
- static getShortPercent(insight) {
377
- return insight ? (insight.sell || 0) : 0;
378
- }
379
-
380
- static getGrowthPercent(insight) {
381
- return insight ? (insight.growth || 0) : 0;
382
- }
383
-
384
- // --- Derived Counts (Estimated) ---
253
+ static getTotalOwners(insight) { return insight ? (insight.total || 0) : 0; }
254
+ static getLongPercent(insight) { return insight ? (insight.buy || 0) : 0; }
255
+ static getShortPercent(insight) { return insight ? (insight.sell || 0) : 0; }
256
+ static getGrowthPercent(insight) { return insight ? (insight.growth || 0) : 0; }
385
257
 
386
258
  static getLongCount(insight) {
387
259
  const total = this.getTotalOwners(insight);
@@ -395,19 +267,11 @@ class InsightsExtractor {
395
267
  return Math.floor(total * (sellPct / 100));
396
268
  }
397
269
 
398
- /**
399
- * Calculates the net change in users from yesterday based on growth %.
400
- * Formula: NetChange = Total - (Total / (1 + Growth/100))
401
- */
402
270
  static getNetOwnershipChange(insight) {
403
271
  const total = this.getTotalOwners(insight);
404
272
  const growth = this.getGrowthPercent(insight);
405
273
  if (total === 0) return 0;
406
-
407
- // Reverse engineer yesterday's count
408
- // Today = Yesterday * (1 + growth)
409
- // Yesterday = Today / (1 + growth)
410
- const prevTotal = total / (1 + (growth / 100)); // TODO: Check precision issues
274
+ const prevTotal = total / (1 + (growth / 100));
411
275
  return Math.round(total - prevTotal);
412
276
  }
413
277
  }
@@ -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.212",
3
+ "version": "1.0.214",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [