bulltrackers-module 1.0.170 → 1.0.172
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/functions/computation-system/controllers/computation_controller.js +215 -0
- package/functions/computation-system/helpers/computation_pass_runner.js +10 -2
- package/functions/computation-system/helpers/orchestration_helpers.js +191 -652
- package/functions/computation-system/layers/math_primitives.js +346 -0
- package/functions/task-engine/helpers/discover_helpers.js +33 -51
- package/functions/task-engine/helpers/update_helpers.js +57 -159
- package/functions/task-engine/helpers/verify_helpers.js +27 -44
- package/package.json +1 -5
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Math Layer - Single Source of Truth (V3 Final)
|
|
3
|
+
* Encapsulates Schema Knowledge, Mathematical Primitives, and Signal Extractors.
|
|
4
|
+
* * STRICT COMPLIANCE:
|
|
5
|
+
* - Adheres to 'schema.md' definitions for Normal vs Speculator portfolios.
|
|
6
|
+
* - standardizes access to P&L, Weights, and Rates.
|
|
7
|
+
* - Provides safe fallbacks for all fields.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const SCHEMAS = {
|
|
11
|
+
USER_TYPES: { NORMAL: 'normal', SPECULATOR: 'speculator' }
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
class DataExtractor {
|
|
15
|
+
// ========================================================================
|
|
16
|
+
// 1. COLLECTION ACCESSORS
|
|
17
|
+
// ========================================================================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Extract positions array based on User Type.
|
|
21
|
+
* - Normal: Uses 'AggregatedPositions' (Grouped by Asset + Direction)
|
|
22
|
+
* - Speculator: Uses 'PublicPositions' (Individual Trades)
|
|
23
|
+
*/
|
|
24
|
+
static getPositions(portfolio, userType) {
|
|
25
|
+
if (!portfolio) return [];
|
|
26
|
+
|
|
27
|
+
if (userType === SCHEMAS.USER_TYPES.SPECULATOR) {
|
|
28
|
+
return portfolio.PublicPositions || [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Default to Normal User Schema
|
|
32
|
+
return portfolio.AggregatedPositions || [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ========================================================================
|
|
36
|
+
// 2. IDENTITY & KEYS
|
|
37
|
+
// ========================================================================
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Extract standardized Instrument ID.
|
|
41
|
+
*/
|
|
42
|
+
static getInstrumentId(position) {
|
|
43
|
+
if (!position) return null;
|
|
44
|
+
// Handle string or number variations safely
|
|
45
|
+
return position.InstrumentID || position.instrumentId || null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Extract a unique Identifier for the position.
|
|
50
|
+
* - Speculator: Uses 'PositionID'.
|
|
51
|
+
* - Normal: Generates Composite Key (InstrumentID_Direction) since they lack unique Trade IDs.
|
|
52
|
+
*/
|
|
53
|
+
static getPositionId(position) {
|
|
54
|
+
if (!position) return null;
|
|
55
|
+
|
|
56
|
+
// 1. Try Explicit ID (Speculators)
|
|
57
|
+
if (position.PositionID) return String(position.PositionID);
|
|
58
|
+
if (position.PositionId) return String(position.PositionId);
|
|
59
|
+
|
|
60
|
+
// 2. Fallback to Composite Key (Normal Users)
|
|
61
|
+
const instId = this.getInstrumentId(position);
|
|
62
|
+
const dir = this.getDirection(position);
|
|
63
|
+
if (instId) return `${instId}_${dir}`;
|
|
64
|
+
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ========================================================================
|
|
69
|
+
// 3. FINANCIAL METRICS (WEIGHTS & P&L)
|
|
70
|
+
// ========================================================================
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Extract Net Profit %.
|
|
74
|
+
* Schema: 'NetProfit' is the percentage profit relative to invested capital.
|
|
75
|
+
*/
|
|
76
|
+
static getNetProfit(position) {
|
|
77
|
+
return position ? (position.NetProfit || 0) : 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Extract Position Weight (Allocation %).
|
|
82
|
+
* Schema:
|
|
83
|
+
* - Normal: 'Invested' is % of initial capital.
|
|
84
|
+
* - Speculator: 'Invested' (or 'Amount' in some contexts) is % of initial capital.
|
|
85
|
+
*/
|
|
86
|
+
static getPositionWeight(position, userType) {
|
|
87
|
+
if (!position) return 0;
|
|
88
|
+
|
|
89
|
+
// Both schemas use 'Invested' to represent the allocation percentage.
|
|
90
|
+
// Speculators might optionally have 'Amount', we prioritize 'Invested' for consistency.
|
|
91
|
+
return position.Invested || position.Amount || 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Extract Current Equity Value %.
|
|
96
|
+
* Schema: 'Value' is the current value as a % of total portfolio equity.
|
|
97
|
+
*/
|
|
98
|
+
static getPositionValuePct(position) {
|
|
99
|
+
return position ? (position.Value || 0) : 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ========================================================================
|
|
103
|
+
// 4. PORTFOLIO LEVEL SUMMARY
|
|
104
|
+
// ========================================================================
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Calculate/Extract Daily Portfolio P&L %.
|
|
108
|
+
*/
|
|
109
|
+
static getPortfolioDailyPnl(portfolio, userType) {
|
|
110
|
+
if (!portfolio) return 0;
|
|
111
|
+
|
|
112
|
+
// 1. Speculator (Explicit 'NetProfit' field on root)
|
|
113
|
+
if (userType === SCHEMAS.USER_TYPES.SPECULATOR) {
|
|
114
|
+
return portfolio.NetProfit || 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 2. Normal (Aggregated Calculation)
|
|
118
|
+
// Logic: Sum(Value - Invested) across the 'InstrumentType' breakdown
|
|
119
|
+
// This gives the net performance change for the day relative to the portfolio.
|
|
120
|
+
if (portfolio.AggregatedPositionsByInstrumentTypeID) {
|
|
121
|
+
return portfolio.AggregatedPositionsByInstrumentTypeID.reduce((sum, agg) => {
|
|
122
|
+
return sum + ((agg.Value || 0) - (agg.Invested || 0));
|
|
123
|
+
}, 0);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ========================================================================
|
|
130
|
+
// 5. TRADE DETAILS (SPECULATOR SPECIFIC)
|
|
131
|
+
// ========================================================================
|
|
132
|
+
|
|
133
|
+
static getDirection(position) {
|
|
134
|
+
if (!position) return "Buy";
|
|
135
|
+
if (position.Direction) return position.Direction;
|
|
136
|
+
if (typeof position.IsBuy === 'boolean') return position.IsBuy ? "Buy" : "Sell";
|
|
137
|
+
return "Buy"; // Default
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
static getLeverage(position) {
|
|
141
|
+
return position ? (position.Leverage || 1) : 1;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
static getOpenRate(position) {
|
|
145
|
+
return position ? (position.OpenRate || 0) : 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
static getCurrentRate(position) {
|
|
149
|
+
return position ? (position.CurrentRate || 0) : 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
static getStopLossRate(position) {
|
|
153
|
+
return position ? (position.StopLossRate || 0) : 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
static getTakeProfitRate(position) {
|
|
157
|
+
return position ? (position.TakeProfitRate || 0) : 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
static getHasTSL(position) {
|
|
161
|
+
return position ? (position.HasTrailingStopLoss === true) : false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Extract Open Date Time.
|
|
166
|
+
* Used for bagholder calculations.
|
|
167
|
+
*/
|
|
168
|
+
static getOpenDateTime(position) {
|
|
169
|
+
if (!position || !position.OpenDateTime) return null;
|
|
170
|
+
return new Date(position.OpenDateTime);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
class HistoryExtractor {
|
|
175
|
+
// --- Schema Accessor (NEW) ---
|
|
176
|
+
/**
|
|
177
|
+
* Extracts the daily history snapshot from the User object.
|
|
178
|
+
* This decouples the computation from knowing 'user.history.today'.
|
|
179
|
+
*/
|
|
180
|
+
static getDailyHistory(user) {
|
|
181
|
+
return user?.history?.today || null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// --- Data Extractors ---
|
|
185
|
+
static getTradedAssets(historyDoc) {
|
|
186
|
+
if (!historyDoc || !Array.isArray(historyDoc.assets)) return [];
|
|
187
|
+
return historyDoc.assets;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
static getInstrumentId(asset) {
|
|
191
|
+
return asset ? asset.instrumentId : null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
static getAvgHoldingTimeMinutes(asset) {
|
|
195
|
+
return asset ? (asset.avgHoldingTimeInMinutes || 0) : 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
static getSummary(historyDoc) {
|
|
199
|
+
const all = historyDoc?.all;
|
|
200
|
+
if (!all) return null;
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
totalTrades: all.totalTrades || 0,
|
|
204
|
+
winRatio: all.winRatio || 0,
|
|
205
|
+
avgProfitPct: all.avgProfitPct || 0,
|
|
206
|
+
avgLossPct: all.avgLossPct || 0,
|
|
207
|
+
avgHoldingTimeInMinutes: all.avgHoldingTimeInMinutes || 0
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
class SignalPrimitives {
|
|
213
|
+
/**
|
|
214
|
+
* Safely extracts a specific numeric field for a specific ticker from a dependency.
|
|
215
|
+
*/
|
|
216
|
+
static getMetric(dependencies, calcName, ticker, fieldName, fallback = 0) {
|
|
217
|
+
if (!dependencies || !dependencies[calcName]) return fallback;
|
|
218
|
+
const tickerData = dependencies[calcName][ticker];
|
|
219
|
+
if (!tickerData) return fallback;
|
|
220
|
+
|
|
221
|
+
const val = tickerData[fieldName];
|
|
222
|
+
return (typeof val === 'number') ? val : fallback;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Creates a unified Set of all keys (tickers) present across multiple dependency results.
|
|
227
|
+
*/
|
|
228
|
+
static getUnionKeys(dependencies, calcNames) {
|
|
229
|
+
const keys = new Set();
|
|
230
|
+
if (!dependencies) return [];
|
|
231
|
+
|
|
232
|
+
for (const name of calcNames) {
|
|
233
|
+
const resultObj = dependencies[name];
|
|
234
|
+
if (resultObj && typeof resultObj === 'object') {
|
|
235
|
+
Object.keys(resultObj).forEach(k => keys.add(k));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return Array.from(keys);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Hyperbolic Tangent Normalization.
|
|
243
|
+
* Maps inputs to a strict -Scale to +Scale range.
|
|
244
|
+
*/
|
|
245
|
+
static normalizeTanh(value, scale = 10, sensitivity = 10.0) {
|
|
246
|
+
if (value === 0) return 0;
|
|
247
|
+
return Math.tanh(value / sensitivity) * scale;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Standard Z-Score normalization.
|
|
252
|
+
*/
|
|
253
|
+
static normalizeZScore(value, mean, stdDev) {
|
|
254
|
+
if (!stdDev || stdDev === 0) return 0;
|
|
255
|
+
return (value - mean) / stdDev;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Simple Divergence (A - B).
|
|
260
|
+
*/
|
|
261
|
+
static divergence(valueA, valueB) {
|
|
262
|
+
return (valueA || 0) - (valueB || 0);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
class MathPrimitives {
|
|
267
|
+
static average(values) {
|
|
268
|
+
if (!values || !values.length) return 0;
|
|
269
|
+
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
static median(values) {
|
|
273
|
+
if (!values || !values.length) return 0;
|
|
274
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
275
|
+
const mid = Math.floor(sorted.length / 2);
|
|
276
|
+
return sorted.length % 2 === 0
|
|
277
|
+
? (sorted[mid - 1] + sorted[mid]) / 2
|
|
278
|
+
: sorted[mid];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
static standardDeviation(values) {
|
|
282
|
+
if (!values || !values.length) return 0;
|
|
283
|
+
const avg = this.average(values);
|
|
284
|
+
const squareDiffs = values.map(val => Math.pow((val || 0) - avg, 2));
|
|
285
|
+
return Math.sqrt(this.average(squareDiffs));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
static bucketBinary(value, threshold = 0) {
|
|
289
|
+
return value > threshold ? 'winner' : 'loser';
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
class Aggregators {
|
|
294
|
+
/**
|
|
295
|
+
* Helper to bucket users by P&L Status.
|
|
296
|
+
* Used by legacy systems or specific aggregators.
|
|
297
|
+
*/
|
|
298
|
+
static bucketUsersByPnlPerAsset(usersData, tickerMap) {
|
|
299
|
+
const buckets = new Map();
|
|
300
|
+
for (const [userId, portfolio] of Object.entries(usersData)) {
|
|
301
|
+
// Auto-detect type if not provided (Legacy compatibility)
|
|
302
|
+
const userType = portfolio.PublicPositions ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL;
|
|
303
|
+
const positions = DataExtractor.getPositions(portfolio, userType);
|
|
304
|
+
|
|
305
|
+
for (const pos of positions) {
|
|
306
|
+
const id = DataExtractor.getInstrumentId(pos);
|
|
307
|
+
const pnl = DataExtractor.getNetProfit(pos);
|
|
308
|
+
if (!id || pnl === 0) continue;
|
|
309
|
+
|
|
310
|
+
const ticker = tickerMap[id];
|
|
311
|
+
if (!ticker) continue;
|
|
312
|
+
|
|
313
|
+
if (!buckets.has(ticker)) buckets.set(ticker, { winners: [], losers: [] });
|
|
314
|
+
const b = buckets.get(ticker);
|
|
315
|
+
|
|
316
|
+
if (pnl > 0) b.winners.push(userId);
|
|
317
|
+
else b.losers.push(userId);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return Object.fromEntries(buckets);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
class Validators {
|
|
325
|
+
static validatePortfolio(portfolio, userType) {
|
|
326
|
+
if (!portfolio) return { valid: false, errors: ['Portfolio is null'] };
|
|
327
|
+
|
|
328
|
+
if (userType === SCHEMAS.USER_TYPES.SPECULATOR) {
|
|
329
|
+
if (!portfolio.PublicPositions) return { valid: false, errors: ['Missing PublicPositions'] };
|
|
330
|
+
} else {
|
|
331
|
+
if (!portfolio.AggregatedPositions) return { valid: false, errors: ['Missing AggregatedPositions'] };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return { valid: true, errors: [] };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
module.exports = {
|
|
339
|
+
SCHEMAS,
|
|
340
|
+
DataExtractor,
|
|
341
|
+
HistoryExtractor,
|
|
342
|
+
MathPrimitives,
|
|
343
|
+
Aggregators,
|
|
344
|
+
Validators,
|
|
345
|
+
SignalPrimitives
|
|
346
|
+
};
|
|
@@ -24,8 +24,7 @@ async function handleDiscover(task, taskId, dependencies, config) {
|
|
|
24
24
|
const url = `${config.ETORO_API_RANKINGS_URL}?Period=LastTwoYears`;
|
|
25
25
|
|
|
26
26
|
const selectedHeader = await headerManager.selectHeader();
|
|
27
|
-
if (!selectedHeader) {
|
|
28
|
-
logger.log('ERROR', `[DISCOVER/${taskId}] Could not select a header. Aborting task.`);
|
|
27
|
+
if (!selectedHeader) { logger.log('ERROR', `[DISCOVER/${taskId}] Could not select a header. Aborting task.`);
|
|
29
28
|
throw new Error("Could not select a header.");
|
|
30
29
|
}
|
|
31
30
|
|
|
@@ -35,45 +34,34 @@ async function handleDiscover(task, taskId, dependencies, config) {
|
|
|
35
34
|
|
|
36
35
|
try { // Outer try for the whole operation
|
|
37
36
|
let response;
|
|
38
|
-
const options = {
|
|
39
|
-
method: 'POST',
|
|
40
|
-
headers: { ...selectedHeader.header, 'Content-Type': 'application/json' },
|
|
41
|
-
body: JSON.stringify(cids),
|
|
42
|
-
};
|
|
37
|
+
const options = { method: 'POST', headers: { ...selectedHeader.header, 'Content-Type': 'application/json' }, body: JSON.stringify(cids), };
|
|
43
38
|
|
|
44
39
|
logger.log('INFO', `${logPrefix} Starting discovery for ${cids.length} CIDs. Block: ${blockId}, Type: ${userType}`);
|
|
45
40
|
|
|
46
41
|
try {
|
|
47
42
|
// --- REFACTOR 3: ADD FALLBACK ---
|
|
48
|
-
logger.log('TRACE', `${logPrefix} Attempting discovery fetch via AppScript proxy...`);
|
|
43
|
+
logger.log('TRACE', `${logPrefix} Attempting discovery fetch via AppScript proxy...`);
|
|
49
44
|
response = await proxyManager.fetch(url, options);
|
|
50
|
-
if (!response.ok) throw new Error(`AppScript proxy failed with status ${response.status}`);
|
|
45
|
+
if (!response.ok) throw new Error(`AppScript proxy failed with status ${response.status}`); // Failure
|
|
51
46
|
|
|
52
|
-
} catch (proxyError) {
|
|
53
|
-
logger.log('WARN', `${logPrefix} AppScript proxy fetch FAILED. Error: ${proxyError.message}. Attempting direct node-fetch fallback.`, {
|
|
54
|
-
error: proxyError.message,
|
|
55
|
-
source: 'AppScript'
|
|
56
|
-
});
|
|
47
|
+
} catch (proxyError) { // Fuck we failed with appscript IP Pools, log error type
|
|
48
|
+
logger.log('WARN', `${logPrefix} AppScript proxy fetch FAILED. Error: ${proxyError.message}. Attempting direct node-fetch fallback.`, { error: proxyError.message,source: 'AppScript' });
|
|
57
49
|
proxyUsed = false;
|
|
58
50
|
|
|
59
51
|
try {
|
|
60
|
-
response = await fetch(url, options); // Direct node-fetch
|
|
61
|
-
if (!response.ok) {
|
|
62
|
-
|
|
63
|
-
throw new Error(`Direct fetch failed with status ${response.status}. Response: ${errorText.substring(0, 200)}`);
|
|
52
|
+
response = await fetch(url, options); // Direct node-fetch fallback, use GCP IP Pools
|
|
53
|
+
if (!response.ok) { const errorText = await response.text();
|
|
54
|
+
throw new Error(`Direct fetch failed with status ${response.status}. Response: ${errorText.substring(0, 200)}`); // Fuck we failed here too.
|
|
64
55
|
}
|
|
65
56
|
|
|
66
|
-
} catch (fallbackError) {
|
|
67
|
-
logger.log('ERROR', `${logPrefix} Direct node-fetch fallback FAILED.`, {
|
|
68
|
-
error: fallbackError.message,
|
|
69
|
-
source: 'eToro/Network'
|
|
70
|
-
});
|
|
57
|
+
} catch (fallbackError) { // Figure out the error type
|
|
58
|
+
logger.log('ERROR', `${logPrefix} Direct node-fetch fallback FAILED.`, { error: fallbackError.message, source: 'eToro/Network' });
|
|
71
59
|
throw fallbackError; // Throw to be caught by outer try
|
|
72
60
|
}
|
|
73
61
|
// --- END REFACTOR 3 ---
|
|
74
62
|
}
|
|
75
63
|
|
|
76
|
-
// --- If we are here, `response` is valid ---
|
|
64
|
+
// --- If we are here, `response` is valid and we are very smart ---
|
|
77
65
|
|
|
78
66
|
// Step 1. Discover Speculators
|
|
79
67
|
if (userType === 'speculator') {
|
|
@@ -87,17 +75,12 @@ async function handleDiscover(task, taskId, dependencies, config) {
|
|
|
87
75
|
// --- REFACTOR 4: LOG RAW RESPONSE ON PARSE FAILURE ---
|
|
88
76
|
publicUsers = JSON.parse(body);
|
|
89
77
|
} catch (parseError) {
|
|
90
|
-
logger.log('ERROR', `${logPrefix} FAILED TO PARSE JSON RESPONSE. RAW BODY:`, {
|
|
91
|
-
parseErrorMessage: parseError.message,
|
|
92
|
-
rawResponseText: body
|
|
93
|
-
});
|
|
78
|
+
logger.log('ERROR', `${logPrefix} FAILED TO PARSE JSON RESPONSE. RAW BODY:`, { parseErrorMessage: parseError.message, rawResponseText: body });
|
|
94
79
|
throw new Error(`Failed to parse JSON response from discovery API. Body: ${body.substring(0, 200)}`);
|
|
95
80
|
}
|
|
96
81
|
// --- END REFACTOR 4 ---
|
|
97
82
|
|
|
98
|
-
if (!Array.isArray(publicUsers)) {
|
|
99
|
-
logger.log('WARN', `${logPrefix} API returned non-array response. Type: ${typeof publicUsers}`);
|
|
100
|
-
wasSuccess = true; // API call worked, data was just empty/weird
|
|
83
|
+
if (!Array.isArray(publicUsers)) { logger.log('WARN', `${logPrefix} API returned non-array response. Type: ${typeof publicUsers}`); wasSuccess = true; // API call worked, data was just empty/weird
|
|
101
84
|
return;
|
|
102
85
|
}
|
|
103
86
|
|
|
@@ -110,9 +93,9 @@ async function handleDiscover(task, taskId, dependencies, config) {
|
|
|
110
93
|
// Step 2. Filter active users
|
|
111
94
|
const preliminaryActiveUsers = publicUsers.filter(user =>
|
|
112
95
|
new Date(user.Value.LastActivity) > oneMonthAgo &&
|
|
113
|
-
user.Value.DailyGain !== 0 &&
|
|
114
|
-
user.Value.Exposure !== 0 &&
|
|
115
|
-
user.Value.RiskScore !== 0
|
|
96
|
+
user.Value.DailyGain !== 0 && // Daily % gain, if 0 then they can't hold positions --- or its a weekend and portfolio holds solely stocks
|
|
97
|
+
user.Value.Exposure !== 0 && // Not sure what this means exactly but exposure should refer to position % held/invested in
|
|
98
|
+
user.Value.RiskScore !== 0 // If portfolio has risk score 0, they cannot have positions
|
|
116
99
|
);
|
|
117
100
|
logger.log('INFO', `${logPrefix} Found ${preliminaryActiveUsers.length} preliminary active users.`);
|
|
118
101
|
|
|
@@ -136,26 +119,27 @@ async function handleDiscover(task, taskId, dependencies, config) {
|
|
|
136
119
|
const nonSpeculatorCids = [];
|
|
137
120
|
for (const user of preliminaryActiveUsers) {
|
|
138
121
|
const v = user.Value;
|
|
139
|
-
const totalLeverage = (v.MediumLeveragePct || 0) + (v.HighLeveragePct || 0);
|
|
140
|
-
|
|
122
|
+
const totalLeverage = (v.MediumLeveragePct || 0) + (v.HighLeveragePct || 0); // Do they own at least some leveraged positions?
|
|
123
|
+
// We don't know what they are, but we can see their total average leverage, if not 0 on medoum/high leverage, then they MUST have at least 1 leveraged position
|
|
124
|
+
// Note : Medium leverage and high leverage are somewhat arbitrary. Low leverage is presumed to be mean "1" and thus medium leverage is the middle of the available leverage ranges, for stocks this is usually 2, for other assets this can be 5/10 or 20, it varies. High leverage is presumed to be the max leverage for that asset
|
|
125
|
+
const isLikelySpeculator = ((v.Trades || 0) > 500 || (v.TotalTradedInstruments || 0) > 50 || totalLeverage > 50 || (v.WeeklyDD || 0) < -25); // Apply filtering on their total leverage, drawdown and trade numbers to figure out if they are likely a speculative portfolio.
|
|
141
126
|
|
|
142
|
-
if (isLikelySpeculator) {
|
|
143
|
-
finalActiveUsers.push(user);
|
|
144
|
-
} else {
|
|
145
|
-
nonSpeculatorCids.push(user.CID);
|
|
146
|
-
}
|
|
127
|
+
if (isLikelySpeculator) { finalActiveUsers.push(user); } else { nonSpeculatorCids.push(user.CID); }
|
|
147
128
|
}
|
|
148
|
-
invalidCidsToLog.push(...nonSpeculatorCids);
|
|
129
|
+
invalidCidsToLog.push(...nonSpeculatorCids); //
|
|
130
|
+
// This logs how many users did NOT pass SPECIFICALLY the islikelyspeculator
|
|
149
131
|
logger.log('INFO', `${logPrefix} Speculator pre-filter complete. ${finalActiveUsers.length} users passed. ${nonSpeculatorCids.length} users failed heuristic.`);
|
|
150
132
|
|
|
151
|
-
if (invalidCidsToLog.length > 0) {
|
|
133
|
+
if (invalidCidsToLog.length > 0) { // Log the number of users we tried for speculators that failed the filtering checks.
|
|
134
|
+
// NOTE : This logs the users who failed ANY of the 3 filters (private users, inactive public users, or users who didn't meet the specific conditions for islikelyspeculator)
|
|
152
135
|
await pubsub.topic(config.PUBSUB_TOPIC_INVALID_SPECULATOR_LOG).publishMessage({ json: { invalidCids: invalidCidsToLog } });
|
|
153
136
|
logger.log('INFO', `${logPrefix} Reported ${invalidCidsToLog.length} invalid (private, inactive, or failed heuristic) speculator IDs.`);
|
|
154
137
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
138
|
+
// Step 5. For non-speculator tasks, we keep all users who passed the
|
|
139
|
+
// “active user” filters. Unlike speculators, normal users do NOT require
|
|
140
|
+
// the speculative-heuristic (leverage / trades / drawdown) filter —
|
|
141
|
+
// any public & active user is valid.
|
|
142
|
+
} else { finalActiveUsers = preliminaryActiveUsers; }
|
|
159
143
|
|
|
160
144
|
// Step 6. Publish 'verify' task
|
|
161
145
|
if (finalActiveUsers.length > 0) {
|
|
@@ -168,12 +152,10 @@ async function handleDiscover(task, taskId, dependencies, config) {
|
|
|
168
152
|
};
|
|
169
153
|
await pubsub.topic(config.PUBSUB_TOPIC_USER_FETCH).publishMessage({ json: { tasks: [verificationTask] } });
|
|
170
154
|
logger.log('SUCCESS', `${logPrefix} Chaining to 'verify' task for ${finalActiveUsers.length} active users.`);
|
|
171
|
-
} else {
|
|
172
|
-
logger.log('INFO', `${logPrefix} No active users found to verify.`);
|
|
173
|
-
}
|
|
155
|
+
} else { logger.log('INFO', `${logPrefix} No active users found to verify.`); }
|
|
174
156
|
|
|
175
157
|
} catch (err) {
|
|
176
|
-
logger.log('ERROR', `${logPrefix} FATAL error processing discovery task.`, { errorMessage: err.message, errorStack: err.stack });
|
|
158
|
+
logger.log('ERROR', `${logPrefix} FATAL error processing discovery task.`, { errorMessage: err.message, errorStack: err.stack }); // We fucked up
|
|
177
159
|
wasSuccess = false; // Ensure it's marked as failure
|
|
178
160
|
} finally {
|
|
179
161
|
if (selectedHeader && proxyUsed) {
|