bulltrackers-module 1.0.206 → 1.0.208

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.
@@ -12,18 +12,30 @@
12
12
  */
13
13
  async function checkDiscoveryNeed(userType, config, dependencies) {
14
14
  const { logger, firestoreUtils } = dependencies;
15
+ const { targetUsersPerBlock, normalBlockCountsDocPath, allHighValueBlocks } = config;
16
+
17
+ // --- REFACTOR: ORGANIC SPECULATOR DISCOVERY ---
18
+ // We no longer "hunt" for speculators using block targets.
19
+ // Speculators are now discovered organically via the Normal User Update Loop (Feedback Loop).
20
+ if (userType === 'speculator') {
21
+ logger.log('INFO', '[Orchestrator Helpers] Speculator discovery is Organic/Feedback-Driven. Skipping active discovery checks.');
22
+ return { needsDiscovery: false, blocksToFill: [] };
23
+ }
24
+
15
25
  logger.log('INFO', `[Orchestrator Helpers] Checking discovery need for ${userType}...`);
16
- const { targetUsersPerBlock, speculatorBlockCountsDocPath, normalBlockCountsDocPath,speculatorInstruments,allHighValueBlocks} = config;
17
- const isSpeculator = userType === 'speculator';
18
- const blockCounts = await firestoreUtils.getBlockCapacities(dependencies, { speculatorBlockCountsDocPath, normalBlockCountsDocPath }, userType);
19
- let underPopulatedBlocks = [];
20
- if (isSpeculator) {for (const instrument of speculatorInstruments) { for (const block of allHighValueBlocks)
21
- {const instrumentBlockKey = `${instrument}_${block.startId}`;
22
- if ((blockCounts[instrumentBlockKey] || 0) < targetUsersPerBlock) {underPopulatedBlocks.push({ ...block, instrument });}}}}
23
- else {underPopulatedBlocks = allHighValueBlocks.filter(block => (blockCounts[block.startId] || 0) < targetUsersPerBlock);}
24
- const needsDiscovery = underPopulatedBlocks.length > 0;
25
- if (!needsDiscovery) {logger.log('SUCCESS', `✅ All blocks are at target capacity for ${userType} users.`);}
26
- else {logger.log('INFO', `Found ${underPopulatedBlocks.length} underpopulated blocks/instruments for ${userType}.`);}
26
+
27
+ // Logic for Normal Users remains (fill blocks to target)
28
+ const blockCounts = await firestoreUtils.getBlockCapacities(dependencies, { normalBlockCountsDocPath }, userType);
29
+
30
+ const underPopulatedBlocks = allHighValueBlocks.filter(block => (blockCounts[block.startId] || 0) < targetUsersPerBlock);
31
+ const needsDiscovery = underPopulatedBlocks.length > 0;
32
+
33
+ if (!needsDiscovery) {
34
+ logger.log('SUCCESS', `✅ All blocks are at target capacity for ${userType} users.`);
35
+ } else {
36
+ logger.log('INFO', `Found ${underPopulatedBlocks.length} underpopulated blocks for ${userType}.`);
37
+ }
38
+
27
39
  return { needsDiscovery, blocksToFill: underPopulatedBlocks };
28
40
  }
29
41
 
@@ -38,29 +50,70 @@ async function checkDiscoveryNeed(userType, config, dependencies) {
38
50
  async function getDiscoveryCandidates(userType, blocksToFill, config, dependencies) {
39
51
  const { logger, firestoreUtils } = dependencies;
40
52
  logger.log('INFO', `[Orchestrator Helpers] Getting discovery candidates for ${userType}...`);
41
- const {maxTasksPerRun,discoveryBatchSize,maxRandomCidsToDiscover,specBlocksCollection,pendingSpecCollection,normalUserCollection,invalidSpecCollection,speculatorInstrumentSet,snapshotsSubCollectionName,partsSubCollectionName} = config;
42
- const isSpeculator = userType === 'speculator';
53
+
54
+ const {
55
+ maxTasksPerRun,
56
+ discoveryBatchSize,
57
+ maxRandomCidsToDiscover,
58
+ specBlocksCollection,
59
+ pendingSpecCollection,
60
+ normalUserCollection,
61
+ invalidSpecCollection,
62
+ snapshotsSubCollectionName,
63
+ partsSubCollectionName
64
+ } = config;
65
+
43
66
  const dispatchedCids = new Set();
44
- const latestNormalPortfolios = await firestoreUtils.getLatestNormalUserPortfolios(dependencies,{ normalUserCollectionName: normalUserCollection, snapshotsSubCollectionName, partsSubCollectionName });
67
+
68
+ // Fetch exclusion list to avoid re-discovering existing users
69
+ // Note: For Normal users, we check against existing Normal users.
70
+ const latestNormalPortfolios = await firestoreUtils.getLatestNormalUserPortfolios(dependencies, {
71
+ normalUserCollectionName: normalUserCollection,
72
+ snapshotsSubCollectionName,
73
+ partsSubCollectionName
74
+ });
75
+
45
76
  const existingNormalUserIds = new Set(Object.keys(latestNormalPortfolios));
46
- const exclusionIds = await firestoreUtils.getExclusionIds(dependencies, {specBlocksCollection, pendingSpecCollection, normalUserCollection,invalidSpecCollection,existingNormalUserIds}, userType);
47
- if (isSpeculator) {const prioritizedCandidates = await firestoreUtils.getPrioritizedSpeculators(dependencies,exclusionIds, speculatorInstrumentSet,latestNormalPortfolios);
48
- logger.log('INFO', `Found ${prioritizedCandidates.length} potential new speculators from existing user pool.`);
49
- prioritizedCandidates.forEach(cidStr => {if (dispatchedCids.size < maxTasksPerRun) {dispatchedCids.add(cidStr);}});
50
- logger.log('INFO', `Added ${dispatchedCids.size} prioritized speculators.`);}
77
+ const exclusionIds = await firestoreUtils.getExclusionIds(dependencies, {
78
+ specBlocksCollection,
79
+ pendingSpecCollection,
80
+ normalUserCollection,
81
+ invalidSpecCollection,
82
+ existingNormalUserIds
83
+ }, userType);
84
+
85
+ // --- RANDOM DISCOVERY LOOP (For Normal Users) ---
51
86
  let dispatchedRandomCidCount = 0;
52
- const initialDispatchedCount = dispatchedCids.size;
53
- while ((dispatchedRandomCidCount + initialDispatchedCount) < maxRandomCidsToDiscover &&dispatchedCids.size < maxTasksPerRun && blocksToFill.length > 0){
87
+
88
+ while ((dispatchedRandomCidCount + dispatchedCids.size) < maxRandomCidsToDiscover &&
89
+ dispatchedCids.size < maxTasksPerRun &&
90
+ blocksToFill.length > 0) {
91
+
54
92
  const blockIndex = dispatchedCids.size % blocksToFill.length;
55
93
  const block = blocksToFill[blockIndex];
56
94
  if (!block) break;
95
+
57
96
  for (let j = 0; j < discoveryBatchSize; j++) {
58
- if ((dispatchedRandomCidCount + initialDispatchedCount) >= maxRandomCidsToDiscover || dispatchedCids.size >= maxTasksPerRun) break;
59
- let randomId;
60
- let retryCount = 0;
61
- const MAX_RETRIES = 50;
62
- do {if (++retryCount > MAX_RETRIES) break;randomId = String(Math.floor(Math.random() * 1000000) + block.startId);} while (exclusionIds.has(randomId) ||dispatchedCids.has(randomId));
63
- if (retryCount <= MAX_RETRIES) {dispatchedCids.add(randomId);dispatchedRandomCidCount++;}}}
97
+ if ((dispatchedRandomCidCount + dispatchedCids.size) >= maxRandomCidsToDiscover ||
98
+ dispatchedCids.size >= maxTasksPerRun) break;
99
+
100
+ let randomId;
101
+ let retryCount = 0;
102
+ const MAX_RETRIES = 50;
103
+
104
+ do {
105
+ if (++retryCount > MAX_RETRIES) break;
106
+ // Generate random CID within the block range
107
+ randomId = String(Math.floor(Math.random() * 1000000) + block.startId);
108
+ } while (exclusionIds.has(randomId) || dispatchedCids.has(randomId));
109
+
110
+ if (retryCount <= MAX_RETRIES) {
111
+ dispatchedCids.add(randomId);
112
+ dispatchedRandomCidCount++;
113
+ }
114
+ }
115
+ }
116
+
64
117
  logger.log('INFO', `Generated ${dispatchedRandomCidCount} random CIDs for ${userType} discovery.`);
65
118
  logger.log('INFO', `Total candidates for dispatch: ${dispatchedCids.size}`);
66
119
  return dispatchedCids;
@@ -75,34 +128,54 @@ async function getDiscoveryCandidates(userType, blocksToFill, config, dependenci
75
128
  * @returns {Promise<void>}
76
129
  */
77
130
  async function dispatchDiscovery(userType, candidates, config, dependencies) {
78
- const { logger, firestoreUtils, pubsubUtils, pubsub } = dependencies;
79
- const {topicName,dispatchBatchSize,pubsubBatchSize, pendingSpecCollection, pendingMaxFieldsPerDoc,pendingMaxWritesPerBatch} = config;
80
- if (candidates.size === 0) {logger.log('INFO', `[Orchestrator Helpers] No ${userType} candidates to dispatch.`);return;}
131
+ const { logger, pubsub } = dependencies;
132
+ const { topicName, dispatchBatchSize } = config;
133
+
134
+ if (candidates.size === 0) {
135
+ logger.log('INFO', `[Orchestrator Helpers] No ${userType} candidates to dispatch.`);
136
+ return;
137
+ }
138
+
81
139
  logger.log('INFO', `[Orchestrator Helpers] Dispatching ${candidates.size} discovery tasks for ${userType}...`);
82
- const isSpeculator = userType === 'speculator';
140
+
83
141
  const cidsArray = Array.from(candidates);
84
- if (isSpeculator)
85
- {await firestoreUtils.clearCollection(dependencies, pendingSpecCollection);
86
- await firestoreUtils.batchWriteShardedIds(dependencies, {collectionPath: pendingSpecCollection,items: cidsArray, timestamp: new Date(),maxFieldsPerDoc: pendingMaxFieldsPerDoc,maxWritesPerBatch: pendingMaxWritesPerBatch});}
87
142
  const tasks = [];
88
- for (let i = 0; i < cidsArray.length; i += dispatchBatchSize) {const batchCids = cidsArray.slice(i, i + dispatchBatchSize).map(cid => parseInt(cid));
89
- if (batchCids.length > 0) {
90
- const blockId = Math.floor(batchCids[0] / 1000000) * 1000000;
91
- tasks.push({type: 'discover',cids: batchCids,blockId, userType});}}
143
+
144
+ for (let i = 0; i < cidsArray.length; i += dispatchBatchSize) {
145
+ const batchCids = cidsArray.slice(i, i + dispatchBatchSize).map(cid => parseInt(cid));
146
+ if (batchCids.length > 0) {
147
+ const blockId = Math.floor(batchCids[0] / 1000000) * 1000000;
148
+ tasks.push({
149
+ type: 'discover',
150
+ cids: batchCids,
151
+ blockId,
152
+ userType
153
+ });
154
+ }
155
+ }
156
+
92
157
  const topic = pubsub.topic(topicName);
93
158
  let totalCidsPublished = 0;
94
159
  let messagesPublished = 0;
160
+
95
161
  for (let i = 0; i < tasks.length; i += dispatchBatchSize) {
96
162
  const batchOfTasks = tasks.slice(i, i + dispatchBatchSize);
97
163
  const messagePayload = { tasks: batchOfTasks };
98
- try {await topic.publishMessage({ json: messagePayload });
99
- const cidsInThisMessage = batchOfTasks.reduce((acc, task) => acc + task.cids.length, 0);
100
- totalCidsPublished += cidsInThisMessage;
101
- messagesPublished++;
102
- logger.log('INFO', `[Orchestrator Helpers] Dispatched batch ${messagesPublished} with ${batchOfTasks.length} discover tasks (${cidsInThisMessage} CIDs) as 1 Pub/Sub message.`);}
103
- catch (publishError) {
104
- logger.log('ERROR', `[Orchestrator Helpers] Failed to publish discover batch ${messagesPublished + 1}.`, { error: publishError.message });}}
105
- logger.log('SUCCESS', `[Orchestrator Helpers] Dispatched ${totalCidsPublished} CIDs in ${tasks.length} tasks, grouped into ${messagesPublished} Pub/Sub messages for ${userType} discovery.`);
164
+
165
+ try {
166
+ await topic.publishMessage({ json: messagePayload });
167
+
168
+ const cidsInThisMessage = batchOfTasks.reduce((acc, task) => acc + task.cids.length, 0);
169
+ totalCidsPublished += cidsInThisMessage;
170
+ messagesPublished++;
171
+
172
+ logger.log('INFO', `[Orchestrator Helpers] Dispatched batch ${messagesPublished} with ${batchOfTasks.length} discover tasks (${cidsInThisMessage} CIDs) as 1 Pub/Sub message.`);
173
+ } catch (publishError) {
174
+ logger.log('ERROR', `[Orchestrator Helpers] Failed to publish discover batch ${messagesPublished + 1}.`, { error: publishError.message });
175
+ }
176
+ }
177
+
178
+ logger.log('SUCCESS', `[Orchestrator Helpers] Dispatched ${totalCidsPublished} CIDs in ${tasks.length} tasks for ${userType} discovery.`);
106
179
  }
107
180
 
108
181
  module.exports = { checkDiscoveryNeed, getDiscoveryCandidates, dispatchDiscovery };
@@ -1,5 +1,6 @@
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)
3
4
  * (OPTIMIZED V3: Removed obsolete username lookup logic)
4
5
  * (OPTIMIZED V2: Added "Circuit Breaker" for Proxy failures)
5
6
  * (REFACTORED: Concurrency set to 1, added fallback and verbose logging)
@@ -26,27 +27,54 @@ function shouldTryProxy() {
26
27
  */
27
28
  function recordProxyOutcome(success) {
28
29
  if (success) {
29
- if (_consecutiveProxyFailures > 0) {
30
- // Optional: Only log recovery to reduce noise
31
- }
32
30
  _consecutiveProxyFailures = 0;
33
31
  } else {
34
32
  _consecutiveProxyFailures++;
35
33
  }
36
34
  }
37
35
 
36
+ /**
37
+ * --- NEW HELPER: Speculator Detector ---
38
+ * intersections: (History: Leverage > 1) AND (Portfolio: Currently Owned)
39
+ */
40
+ function detectSpeculatorTargets(historyData, portfolioData) {
41
+ if (!historyData?.PublicHistoryPositions || !portfolioData?.AggregatedPositions) return [];
42
+
43
+ // 1. Identify assets that have EVER been traded with leverage > 1
44
+ const leveragedAssets = new Set();
45
+ for (const pos of historyData.PublicHistoryPositions) {
46
+ if (pos.Leverage > 1 && pos.InstrumentID) {
47
+ leveragedAssets.add(pos.InstrumentID);
48
+ }
49
+ }
50
+
51
+ if (leveragedAssets.size === 0) return [];
52
+
53
+ // 2. Check if the user CURRENTLY owns any of these assets
54
+ const targets = [];
55
+ for (const pos of portfolioData.AggregatedPositions) {
56
+ if (leveragedAssets.has(pos.InstrumentID)) {
57
+ targets.push(pos.InstrumentID);
58
+ }
59
+ }
60
+
61
+ return targets;
62
+ }
63
+
38
64
  /**
39
65
  * (REFACTORED: Fully sequential, verbose logging, node-fetch fallback)
40
66
  */
41
- async function handleUpdate(task, taskId, { logger, headerManager, proxyManager, db, batchManager }, config) {
42
- // Note: 'username' param removed from signature as it is no longer needed.
67
+ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager, db, batchManager, pubsub }, config) {
43
68
  const { userId, instruments, instrumentId, userType } = task;
44
69
  const instrumentsToProcess = userType === 'speculator' ? (instruments || [instrumentId]) : [undefined];
45
70
  const today = new Date().toISOString().slice(0, 10);
46
71
  const portfolioBlockId = `${Math.floor(parseInt(userId) / 1000000)}M`;
47
72
  let isPrivate = false;
48
73
 
49
- // DOWNGRADED TO TRACE
74
+ // Captured data for detection logic
75
+ let capturedHistory = null;
76
+ let capturedPortfolio = null;
77
+
50
78
  logger.log('TRACE', `[handleUpdate/${userId}] Starting update task. Type: ${userType}. Instruments: ${instrumentsToProcess.join(', ')}`);
51
79
 
52
80
  // --- 1. Process History Fetch (Sequentially) ---
@@ -63,11 +91,10 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
63
91
  } else {
64
92
 
65
93
  // --- REFACTOR: New Granular API Logic ---
66
- // No username required. Uses CID (userId) directly.
67
94
  const d = new Date();
68
95
  d.setFullYear(d.getFullYear() - 1);
69
96
  const oneYearAgoStr = d.toISOString();
70
- const uuid = crypto.randomUUID ? crypto.randomUUID() : '0205aca7-bd37-4884-8455-f28ce1add2de'; // Fallback for older nodes
97
+ const uuid = crypto.randomUUID ? crypto.randomUUID() : '0205aca7-bd37-4884-8455-f28ce1add2de';
71
98
 
72
99
  const historyUrl = `https://www.etoro.com/sapi/trade-data-real/history/public/credit/flat?StartTime=${oneYearAgoStr}&PageNumber=1&ItemsPerPage=30000&PublicHistoryPortfolioFilter=&CID=${userId}&client_request_id=${uuid}`;
73
100
  const options = { headers: historyHeader.header };
@@ -79,13 +106,12 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
79
106
  logger.log('TRACE', `[handleUpdate/${userId}] Attempting history fetch via AppScript proxy...`);
80
107
  response = await proxyManager.fetch(historyUrl, options);
81
108
  if (!response.ok) throw new Error(`AppScript proxy failed with status ${response.status}`);
82
-
83
109
  wasHistorySuccess = true;
84
110
  proxyUsedForHistory = true;
85
- recordProxyOutcome(true); // Reset
111
+ recordProxyOutcome(true);
86
112
 
87
113
  } catch (proxyError) {
88
- recordProxyOutcome(false); // Count failure
114
+ recordProxyOutcome(false);
89
115
  logger.log('WARN', `[handleUpdate/${userId}] History fetch via AppScript proxy FAILED. Error: ${proxyError.message}. Failures: ${_consecutiveProxyFailures}/${MAX_PROXY_FAILURES}.`, { error: proxyError.message, source: 'AppScript' });
90
116
  }
91
117
  }
@@ -105,6 +131,7 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
105
131
 
106
132
  if (wasHistorySuccess) {
107
133
  const data = await response.json();
134
+ capturedHistory = data; // Capture for later
108
135
  await batchManager.addToTradingHistoryBatch(userId, portfolioBlockId, today, data, userType);
109
136
  }
110
137
  }
@@ -118,7 +145,7 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
118
145
  }
119
146
 
120
147
  // --- 2. Process Portfolio Fetches (Sequentially) ---
121
- logger.log('TRACE', `[handleUpdate/${userId}] Starting ${instrumentsToProcess.length} sequential portfolio fetches.`); // DOWNGRADED TO TRACE
148
+ logger.log('TRACE', `[handleUpdate/${userId}] Starting ${instrumentsToProcess.length} sequential portfolio fetches.`);
122
149
 
123
150
  for (const instId of instrumentsToProcess) {
124
151
  if (isPrivate) {
@@ -141,13 +168,12 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
141
168
  logger.log('TRACE', `[handleUpdate/${userId}] Attempting portfolio fetch via AppScript proxy...`);
142
169
  response = await proxyManager.fetch(portfolioUrl, options);
143
170
  if (!response.ok) throw new Error(`AppScript proxy failed with status ${response.status}`);
144
-
145
171
  wasPortfolioSuccess = true;
146
172
  proxyUsedForPortfolio = true;
147
- recordProxyOutcome(true); // Reset
173
+ recordProxyOutcome(true);
148
174
 
149
175
  } catch (proxyError) {
150
- recordProxyOutcome(false); // Count failure
176
+ recordProxyOutcome(false);
151
177
  logger.log('WARN', `[handleUpdate/${userId}] Portfolio fetch via Proxy FAILED. Error: ${proxyError.message}. Failures: ${_consecutiveProxyFailures}/${MAX_PROXY_FAILURES}.`, { error: proxyError.message, source: 'AppScript' });
152
178
  }
153
179
  }
@@ -172,8 +198,9 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
172
198
 
173
199
  try {
174
200
  const portfolioJson = JSON.parse(body);
201
+ capturedPortfolio = portfolioJson; // Capture for detection
175
202
  await batchManager.addToPortfolioBatch(userId, portfolioBlockId, today, portfolioJson, userType, instId);
176
- logger.log('TRACE', `[handleUpdate/${userId}] Portfolio processed successfully.`); // DOWNGRADED TO TRACE
203
+ logger.log('TRACE', `[handleUpdate/${userId}] Portfolio processed successfully.`);
177
204
 
178
205
  } catch (parseError) {
179
206
  wasPortfolioSuccess = false;
@@ -186,7 +213,34 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
186
213
  if (proxyUsedForPortfolio) { headerManager.updatePerformance(portfolioHeader.id, wasPortfolioSuccess); }
187
214
  }
188
215
 
189
- // --- 5. Handle Private Users & Timestamps ---
216
+ // --- 5. SPECULATOR DETECTION & QUEUEING (NEW) ---
217
+ // Only run detection if:
218
+ // 1. We are processing a Normal User (userType !== 'speculator')
219
+ // 2. We successfully fetched both history and portfolio
220
+ // 3. We have PubSub available to queue new tasks
221
+ if (userType !== 'speculator' && capturedHistory && capturedPortfolio && pubsub && config.PUBSUB_TOPIC_TASK_ENGINE) {
222
+ try {
223
+ const speculatorAssets = detectSpeculatorTargets(capturedHistory, capturedPortfolio);
224
+ if (speculatorAssets.length > 0) {
225
+ logger.log('INFO', `[handleUpdate/${userId}] DETECTED SPECULATOR BEHAVIOR. Queuing ${speculatorAssets.length} targeted updates.`);
226
+
227
+ const newTasks = speculatorAssets.map(assetId => ({
228
+ type: 'update',
229
+ userType: 'speculator',
230
+ userId: userId,
231
+ instrumentId: assetId
232
+ }));
233
+
234
+ // Publish to Task Engine (Tasks are wrapped in a 'tasks' array payload)
235
+ const dataBuffer = Buffer.from(JSON.stringify({ tasks: newTasks }));
236
+ await pubsub.topic(config.PUBSUB_TOPIC_TASK_ENGINE).publishMessage({ data: dataBuffer });
237
+ }
238
+ } catch (detectionError) {
239
+ logger.log('ERROR', `[handleUpdate/${userId}] Error during Speculator Detection.`, { error: detectionError.message });
240
+ }
241
+ }
242
+
243
+ // --- 6. Handle Private Users & Timestamps ---
190
244
  if (isPrivate) {
191
245
  logger.log('WARN', `[handleUpdate/${userId}] Removing private user from updates.`);
192
246
  for (const instrumentId of instrumentsToProcess) {
@@ -207,7 +261,7 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
207
261
 
208
262
  if (userType === 'speculator') { await batchManager.addSpeculatorTimestampFix(userId, String(Math.floor(userId/1e6)*1e6)); }
209
263
 
210
- logger.log('TRACE', `[handleUpdate/${userId}] Update task finished successfully.`); // DOWNGRADED TO TRACE
264
+ logger.log('TRACE', `[handleUpdate/${userId}] Update task finished successfully.`);
211
265
  }
212
266
 
213
267
  module.exports = { handleUpdate };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.206",
3
+ "version": "1.0.208",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [