bulltrackers-module 1.0.212 → 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.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview
3
- * Dynamic Manifest Builder (v5 - Smart Hashing with Safe Mode)
3
+ * Dynamic Manifest Builder (v5.1 - Smart Hashing with Safe Mode)
4
4
  *
5
5
  * This script builds the computation manifest and generates a "Smart Hash"
6
6
  * for each calculation.
@@ -12,6 +12,9 @@
12
12
  * * SAFE MODE:
13
13
  * If a calculation uses no detectable layer keywords, it defaults to
14
14
  * depending on ALL layers to prevent staleness.
15
+ * * FIXED:
16
+ * - Broadened LAYER_TRIGGERS to detect context usage (e.g. 'math.signals')
17
+ * - Updated isHistorical check to look for 'previousComputed'
15
18
  */
16
19
 
17
20
  const { generateCodeHash } = require('../utils/utils');
@@ -61,17 +64,17 @@ const LAYER_HASHES = {
61
64
  const LAYER_TRIGGERS = {
62
65
  'mathematics': [
63
66
  'math.compute', 'MathPrimitives',
64
- 'math.signals', 'SignalPrimitives',
67
+ 'math.signals', 'SignalPrimitives', 'signals.',
65
68
  'math.aggregate', 'Aggregators',
66
- 'math.timeseries', 'TimeSeries',
67
- 'math.distribution', 'DistributionAnalytics',
69
+ 'math.timeseries', 'TimeSeries', 'timeSeries.',
70
+ 'math.distribution', 'DistributionAnalytics', 'distribution.',
68
71
  'math.financial', 'FinancialEngineering'
69
72
  ],
70
73
  'extractors': [
71
74
  'math.extract', 'DataExtractor',
72
75
  'math.history', 'HistoryExtractor',
73
76
  'math.prices', 'priceExtractor',
74
- 'math.insights', 'InsightsExtractor',
77
+ 'math.insights', 'InsightsExtractor', 'insights.',
75
78
  'math.tradeSeries', 'TradeSeriesBuilder'
76
79
  ],
77
80
  'profiling': [
@@ -171,8 +174,10 @@ function buildManifest(productLinesToRun = [], calculations) {
171
174
  const metadata = Class.getMetadata();
172
175
  const dependencies = Class.getDependencies().map(normalizeName);
173
176
 
174
- if (metadata.isHistorical === true && !Class.toString().includes('yesterday')) {
175
- log.warn(`Calculation "${normalizedName}" marked 'isHistorical' but no 'yesterday' reference found.`);
177
+ // FIX: Updated check to include 'previousComputed'
178
+ const codeStr = Class.toString();
179
+ if (metadata.isHistorical === true && !codeStr.includes('yesterday') && !codeStr.includes('previousComputed')) {
180
+ log.warn(`Calculation "${normalizedName}" marked 'isHistorical' but no 'previousComputed' or 'yesterday' reference found.`);
176
181
  }
177
182
 
178
183
  let finalCategory = folderName;
@@ -183,14 +188,13 @@ function buildManifest(productLinesToRun = [], calculations) {
183
188
  }
184
189
 
185
190
  // --- SMART HASH GENERATION ---
186
- const classCode = Class.toString();
187
- let compositeHashString = generateCodeHash(classCode); // Start with own code
191
+ let compositeHashString = generateCodeHash(codeStr); // Start with own code
188
192
  const usedLayers = [];
189
193
 
190
194
  // 1. Check for specific layer usage
191
195
  for (const [layerName, triggers] of Object.entries(LAYER_TRIGGERS)) {
192
196
  // If code contains any trigger for this layer
193
- if (triggers.some(trigger => classCode.includes(trigger))) {
197
+ if (triggers.some(trigger => codeStr.includes(trigger))) {
194
198
  compositeHashString += LAYER_HASHES[layerName]; // Append Layer Hash
195
199
  usedLayers.push(layerName);
196
200
  }
@@ -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.213",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [