bulltrackers-module 1.0.731 → 1.0.733
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/orchestrator/index.js +19 -17
- package/index.js +8 -29
- package/package.json +6 -5
- package/functions/computation-system/WorkflowOrchestrator.js +0 -213
- package/functions/computation-system/config/monitoring_config.js +0 -31
- package/functions/computation-system/config/validation_overrides.js +0 -10
- package/functions/computation-system/context/ContextFactory.js +0 -132
- package/functions/computation-system/context/ManifestBuilder.js +0 -379
- package/functions/computation-system/data/AvailabilityChecker.js +0 -236
- package/functions/computation-system/data/CachedDataLoader.js +0 -325
- package/functions/computation-system/data/DependencyFetcher.js +0 -455
- package/functions/computation-system/executors/MetaExecutor.js +0 -279
- package/functions/computation-system/executors/PriceBatchExecutor.js +0 -108
- package/functions/computation-system/executors/StandardExecutor.js +0 -465
- package/functions/computation-system/helpers/computation_dispatcher.js +0 -750
- package/functions/computation-system/helpers/computation_worker.js +0 -375
- package/functions/computation-system/helpers/monitor.js +0 -64
- package/functions/computation-system/helpers/on_demand_helpers.js +0 -154
- package/functions/computation-system/layers/extractors.js +0 -1097
- package/functions/computation-system/layers/index.js +0 -40
- package/functions/computation-system/layers/mathematics.js +0 -522
- package/functions/computation-system/layers/profiling.js +0 -537
- package/functions/computation-system/layers/validators.js +0 -170
- package/functions/computation-system/legacy/AvailabilityCheckerOld.js +0 -388
- package/functions/computation-system/legacy/CachedDataLoaderOld.js +0 -357
- package/functions/computation-system/legacy/DependencyFetcherOld.js +0 -478
- package/functions/computation-system/legacy/MetaExecutorold.js +0 -364
- package/functions/computation-system/legacy/StandardExecutorold.js +0 -476
- package/functions/computation-system/legacy/computation_dispatcherold.js +0 -944
- package/functions/computation-system/logger/logger.js +0 -297
- package/functions/computation-system/persistence/ContractValidator.js +0 -81
- package/functions/computation-system/persistence/FirestoreUtils.js +0 -56
- package/functions/computation-system/persistence/ResultCommitter.js +0 -283
- package/functions/computation-system/persistence/ResultsValidator.js +0 -130
- package/functions/computation-system/persistence/RunRecorder.js +0 -142
- package/functions/computation-system/persistence/StatusRepository.js +0 -52
- package/functions/computation-system/reporter_epoch.js +0 -6
- package/functions/computation-system/scripts/UpdateContracts.js +0 -128
- package/functions/computation-system/services/SnapshotService.js +0 -148
- package/functions/computation-system/simulation/Fabricator.js +0 -285
- package/functions/computation-system/simulation/SeededRandom.js +0 -41
- package/functions/computation-system/simulation/SimRunner.js +0 -51
- package/functions/computation-system/system_epoch.js +0 -2
- package/functions/computation-system/tools/BuildReporter.js +0 -531
- package/functions/computation-system/tools/ContractDiscoverer.js +0 -144
- package/functions/computation-system/tools/DeploymentValidator.js +0 -536
- package/functions/computation-system/tools/FinalSweepReporter.js +0 -322
- package/functions/computation-system/topology/HashManager.js +0 -55
- package/functions/computation-system/topology/ManifestLoader.js +0 -47
- package/functions/computation-system/utils/data_loader.js +0 -597
- package/functions/computation-system/utils/schema_capture.js +0 -121
- package/functions/computation-system/utils/utils.js +0 -188
|
@@ -1,1097 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Extractors Layer
|
|
3
|
-
* Core access methods to raw data schemas (Social, Portfolio, History, Rankings, Verification).
|
|
4
|
-
* UPDATED: Aligned with corrected schemas for PublicHistoryPositions and Deep PublicPositions.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const { SCHEMAS } = require('./profiling');
|
|
8
|
-
|
|
9
|
-
class TradeSeriesBuilder {
|
|
10
|
-
static buildReturnSeries(historyTrades) {
|
|
11
|
-
if (!historyTrades || !Array.isArray(historyTrades)) return [];
|
|
12
|
-
// Sort by Close Date Ascending
|
|
13
|
-
const closedTrades = [...historyTrades].sort((a, b) => new Date(a.CloseDateTime) - new Date(b.CloseDateTime));
|
|
14
|
-
return closedTrades.map(t => t.NetProfit);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
static buildCumulativeCurve(returnSeries, startValue = 100) {
|
|
18
|
-
const curve = [startValue];
|
|
19
|
-
let current = startValue;
|
|
20
|
-
for (const ret of returnSeries) {
|
|
21
|
-
// Assuming returns are absolute $ values here based on schema "NetProfit": 73.1666
|
|
22
|
-
current += ret;
|
|
23
|
-
curve.push(current);
|
|
24
|
-
}
|
|
25
|
-
return curve;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
static calculateMaxDrawdown(curve) {
|
|
29
|
-
if (!curve || curve.length < 2) return { maxDrawdownPct: 0, peakIndex: 0, troughIndex: 0 };
|
|
30
|
-
|
|
31
|
-
let maxDrawdown = 0;
|
|
32
|
-
let peakValue = curve[0];
|
|
33
|
-
let peakIndex = 0;
|
|
34
|
-
let tempPeakIndex = 0;
|
|
35
|
-
let troughIndex = 0;
|
|
36
|
-
|
|
37
|
-
for (let i = 1; i < curve.length; i++) {
|
|
38
|
-
const currentVal = curve[i];
|
|
39
|
-
|
|
40
|
-
if (currentVal > peakValue) {
|
|
41
|
-
peakValue = currentVal;
|
|
42
|
-
tempPeakIndex = i;
|
|
43
|
-
} else {
|
|
44
|
-
if (peakValue === 0) continue;
|
|
45
|
-
const drawdown = (peakValue - currentVal) / peakValue;
|
|
46
|
-
if (drawdown > maxDrawdown) {
|
|
47
|
-
maxDrawdown = drawdown;
|
|
48
|
-
peakIndex = tempPeakIndex;
|
|
49
|
-
troughIndex = i;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return { maxDrawdownPct: maxDrawdown * 100, peakIndex, troughIndex };
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
class SocialExtractor {
|
|
59
|
-
static getDiscussions(socialData) {
|
|
60
|
-
return socialData?.discussions || [];
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
static getPostId(discussion) {
|
|
64
|
-
return discussion?.post?.id || discussion?.id || null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
static getMessage(discussion) {
|
|
68
|
-
return discussion?.post?.message?.text || "";
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
static getLanguage(discussion) {
|
|
72
|
-
return discussion?.post?.message?.languageCode || "en";
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
static getCreatedDate(discussion) {
|
|
76
|
-
const dateStr = discussion?.post?.created;
|
|
77
|
-
return dateStr ? new Date(dateStr) : null;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
static getOwner(discussion) {
|
|
81
|
-
return discussion?.post?.owner || null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
static getOwnerUsername(discussion) {
|
|
85
|
-
return discussion?.post?.owner?.username || "Unknown";
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
static isOwnerVerified(discussion) {
|
|
89
|
-
const roles = discussion?.post?.owner?.roles || [];
|
|
90
|
-
return roles.includes('Verified') || roles.includes('PopularInvestor');
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
static getPostContextType(discussion) {
|
|
94
|
-
if (discussion?.post?.tags && discussion.post.tags.length > 0) return 'INSTRUMENT_TAGGED';
|
|
95
|
-
if (discussion?.reason) return 'USER_TIMELINE';
|
|
96
|
-
return 'UNKNOWN';
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
static getRelatedInstruments(discussion) {
|
|
100
|
-
const tags = discussion?.post?.tags || [];
|
|
101
|
-
return tags.map(t => ({
|
|
102
|
-
id: t.market?.internalId || t.market?.id,
|
|
103
|
-
symbol: t.market?.symbolName,
|
|
104
|
-
displayName: t.market?.displayName
|
|
105
|
-
})).filter(i => i.id);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
static getFeedReason(discussion) {
|
|
109
|
-
if (!discussion?.reason) return null;
|
|
110
|
-
return {
|
|
111
|
-
type: discussion.reason.type,
|
|
112
|
-
sourceId: discussion.reason.sourceId
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
static getCommentCount(discussion) {
|
|
117
|
-
return discussion?.summary?.totalCommentsAndReplies || 0;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
static getLikeCount(discussion) {
|
|
121
|
-
return discussion?.emotionsData?.like?.paging?.totalCount || 0;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// [NEW] Added for social accuracy calcs
|
|
125
|
-
static getQualityScore(discussion) {
|
|
126
|
-
return discussion?.aiAnalysis?.qualityScore || 0.5; // Default neutral
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
static getAccuracyFromAnalysis(discussion) {
|
|
130
|
-
return discussion?.aiAnalysis?.accuracyScore || null;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
class PopularInvestorExtractor {
|
|
135
|
-
static getPositions(data) {
|
|
136
|
-
// [FIX] Prioritize DeepPositions (merged from deep shard)
|
|
137
|
-
if (data && Array.isArray(data.DeepPositions)) {
|
|
138
|
-
return data.DeepPositions;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (data && Array.isArray(data.PublicPositions)) {
|
|
142
|
-
return data.PublicPositions;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (data && Array.isArray(data.AggregatedPositions)) {
|
|
146
|
-
return data.AggregatedPositions;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return [];
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
static getEquity(data) {
|
|
153
|
-
return data.Equity || data.CreditByRealizedEquity || 0;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
class DataExtractor {
|
|
158
|
-
static getPositions(portfolio, userType) {
|
|
159
|
-
if (!portfolio) return [];
|
|
160
|
-
|
|
161
|
-
// Speculators/PIs might have the Deep Schema
|
|
162
|
-
if (userType === 'POPULAR_INVESTOR' || userType === SCHEMAS.USER_TYPES.SPECULATOR) {
|
|
163
|
-
return PopularInvestorExtractor.getPositions(portfolio);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Standard/Signed-In Users usually have the Overall Schema
|
|
167
|
-
return portfolio.AggregatedPositions || [];
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
static getInstrumentId(position) {
|
|
171
|
-
if (!position) return null;
|
|
172
|
-
return position.InstrumentID || position.instrumentId || null;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
static getPositionId(position) {
|
|
176
|
-
if (!position) return null;
|
|
177
|
-
// Deep Schema: PositionID
|
|
178
|
-
if (position.PositionID) return String(position.PositionID);
|
|
179
|
-
|
|
180
|
-
// Overall Schema: InstrumentID + Direction (Proxy ID)
|
|
181
|
-
const instId = this.getInstrumentId(position);
|
|
182
|
-
const dir = this.getDirection(position);
|
|
183
|
-
if (instId) return `${instId}_${dir}`;
|
|
184
|
-
return null;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
static getNetProfit(position) {
|
|
188
|
-
return position ? (position.NetProfit || 0) : 0;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
static getPositionWeight(position) {
|
|
192
|
-
if (!position) return 0;
|
|
193
|
-
// Deep: Amount / Invested? Schema shows 'Amount' and 'Invested' at top level.
|
|
194
|
-
// Inside PublicPositions (Deep), it uses 'Amount'.
|
|
195
|
-
// Inside AggregatedPositions (Overall), it uses 'Invested'.
|
|
196
|
-
return position.Invested || position.Amount || 0;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
static getPositionValue(position) {
|
|
200
|
-
return position.Value || 0;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
static getDirection(position) {
|
|
204
|
-
if (!position) return "Buy";
|
|
205
|
-
if (position.Direction) return position.Direction;
|
|
206
|
-
if (typeof position.IsBuy === 'boolean') return position.IsBuy ? "Buy" : "Sell";
|
|
207
|
-
return "Buy";
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
static getLeverage(position) { return position ? (position.Leverage || 1) : 1; }
|
|
211
|
-
static getOpenRate(position) { return position ? (position.OpenRate || 0) : 0; }
|
|
212
|
-
static getCurrentRate(position) { return position ? (position.CurrentRate || 0) : 0; }
|
|
213
|
-
|
|
214
|
-
static getStopLossRate(position) {
|
|
215
|
-
const rate = position ? (position.StopLossRate || 0) : 0;
|
|
216
|
-
if (rate > 0 && rate <= 0.01) return 0;
|
|
217
|
-
if (rate < 0) return 0;
|
|
218
|
-
return rate;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
static getTakeProfitRate(position) {
|
|
222
|
-
const rate = position ? (position.TakeProfitRate || 0) : 0;
|
|
223
|
-
if (rate > 0 && rate <= 0.01) return 0;
|
|
224
|
-
return rate;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
static getOpenDateTime(position) {
|
|
228
|
-
return (!position || !position.OpenDateTime) ? null : new Date(position.OpenDateTime);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Extract current sectors from portfolio positions.
|
|
233
|
-
* @param {Object} portfolio - Portfolio data
|
|
234
|
-
* @param {Object} mappings - Context mappings containing instrumentToSector
|
|
235
|
-
* @param {string} userType - User type
|
|
236
|
-
* @returns {string[]} Array of unique sector names
|
|
237
|
-
*/
|
|
238
|
-
static getCurrentSectors(portfolio, mappings, userType) {
|
|
239
|
-
const positions = this.getPositions(portfolio, userType);
|
|
240
|
-
const sectors = new Set();
|
|
241
|
-
const sectorMap = mappings?.instrumentToSector || {};
|
|
242
|
-
|
|
243
|
-
for (const pos of positions) {
|
|
244
|
-
const instId = this.getInstrumentId(pos);
|
|
245
|
-
if (instId) {
|
|
246
|
-
const sector = sectorMap[instId] || 'Unknown';
|
|
247
|
-
sectors.add(sector);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
return Array.from(sectors);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Extract AUM per asset from portfolio.
|
|
255
|
-
* @param {Object} portfolio - Portfolio data
|
|
256
|
-
* @param {string} userType - User type
|
|
257
|
-
* @returns {Object} Map of instrumentId -> AUM value
|
|
258
|
-
*/
|
|
259
|
-
static getAUMPerAsset(portfolio, userType) {
|
|
260
|
-
const positions = this.getPositions(portfolio, userType);
|
|
261
|
-
const assetMap = new Map();
|
|
262
|
-
|
|
263
|
-
for (const pos of positions) {
|
|
264
|
-
const instId = this.getInstrumentId(pos);
|
|
265
|
-
if (instId) {
|
|
266
|
-
const current = assetMap.get(instId) || 0;
|
|
267
|
-
assetMap.set(instId, current + this.getPositionValue(pos));
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const result = {};
|
|
272
|
-
for (const [instId, value] of assetMap.entries()) {
|
|
273
|
-
result[instId] = value;
|
|
274
|
-
}
|
|
275
|
-
return result;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
class CopyTradingExtractor {
|
|
280
|
-
static getAggregatedMirrors(portfolio) {
|
|
281
|
-
if (!portfolio) return [];
|
|
282
|
-
return portfolio.AggregatedMirrors || [];
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
static getMirrorId(mirror) { return mirror?.MirrorID; }
|
|
286
|
-
static getParentCID(mirror) { return mirror?.ParentCID || null; }
|
|
287
|
-
static getParentUsername(mirror) { return mirror?.ParentUsername; }
|
|
288
|
-
static getInvested(mirror) { return mirror?.Invested || 0; }
|
|
289
|
-
static getNetProfit(mirror) { return mirror?.NetProfit || 0; }
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
class RankingsExtractor {
|
|
293
|
-
static getItems(rankingsData) {
|
|
294
|
-
if (!rankingsData) return [];
|
|
295
|
-
if (rankingsData.Items && Array.isArray(rankingsData.Items)) return rankingsData.Items;
|
|
296
|
-
// Sometimes the root object IS the array in other schemas
|
|
297
|
-
if (Array.isArray(rankingsData)) return rankingsData;
|
|
298
|
-
return [];
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
static getEntry(rankingsData, userId) {
|
|
302
|
-
const items = this.getItems(rankingsData);
|
|
303
|
-
return items.find(r => String(r.CustomerId) === String(userId)) || null;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
static getGain(entry) { return entry?.Gain || 0; }
|
|
307
|
-
static getRiskScore(entry) { return entry?.RiskScore || 0; }
|
|
308
|
-
static getMaxDailyRiskScore(entry) { return entry?.MaxDailyRiskScore || 0; }
|
|
309
|
-
static getMaxMonthlyRiskScore(entry) { return entry?.MaxMonthlyRiskScore || 0; }
|
|
310
|
-
static getCopiers(entry) { return entry?.Copiers || 0; }
|
|
311
|
-
static getAUMTierDesc(entry) { return entry?.AUMTierDesc || "N/A"; }
|
|
312
|
-
// [NEW]
|
|
313
|
-
static getAUMValue(entry) { return entry?.AUMValue || 0; }
|
|
314
|
-
static getTags(entry) { return entry?.Tags || []; }
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
class VerificationExtractor {
|
|
318
|
-
static getProfile(verificationMap, userId) {
|
|
319
|
-
if (!verificationMap) return null;
|
|
320
|
-
return verificationMap[userId] || null;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// [FIX] Now relies on normalized data from loader
|
|
324
|
-
static isVerified(profile) {
|
|
325
|
-
return profile?.isVerified === true;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
static getRealCID(profile) {
|
|
329
|
-
return profile?.cid || null;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
static getUsername(profile) {
|
|
333
|
-
return profile?.username || null;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
static getAboutMe(profile) {
|
|
337
|
-
return profile?.aboutMe || "";
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
static getAboutMeShort(profile) {
|
|
341
|
-
return profile?.aboutMeShort || "";
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
static getRestrictions(profile) {
|
|
345
|
-
return profile?.restrictions || [];
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
static hasRestriction(profile, typeId) {
|
|
349
|
-
const restrictions = this.getRestrictions(profile);
|
|
350
|
-
return restrictions.some(r => r.RestrictionTypeID === typeId);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
class priceExtractor {
|
|
355
|
-
static getHistory(pricesContext, tickerOrId) {
|
|
356
|
-
if (!pricesContext || !pricesContext.history) return [];
|
|
357
|
-
let assetData = pricesContext.history[tickerOrId];
|
|
358
|
-
if (!assetData) {
|
|
359
|
-
const id = Object.keys(pricesContext.history).find(key => {
|
|
360
|
-
const data = pricesContext.history[key];
|
|
361
|
-
return data.ticker === tickerOrId;
|
|
362
|
-
});
|
|
363
|
-
if (id) assetData = pricesContext.history[id];
|
|
364
|
-
}
|
|
365
|
-
if (!assetData || !assetData.prices) return [];
|
|
366
|
-
const priceMap = assetData.prices;
|
|
367
|
-
const sortedDates = Object.keys(priceMap).sort((a, b) => a.localeCompare(b));
|
|
368
|
-
return sortedDates.map(date => ({
|
|
369
|
-
date: date,
|
|
370
|
-
price: priceMap[date]
|
|
371
|
-
})).filter(item => item.price > 0);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
class HistoryExtractor {
|
|
376
|
-
static getTrades(historyDoc) {
|
|
377
|
-
if (historyDoc && Array.isArray(historyDoc.PublicHistoryPositions)) {
|
|
378
|
-
return historyDoc.PublicHistoryPositions;
|
|
379
|
-
}
|
|
380
|
-
return [];
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
static getRealizedEquity(historyDoc) {
|
|
384
|
-
return historyDoc?.CreditByRealizedEquity || 0;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// --- [RESTORED METHOD] ---
|
|
388
|
-
static getDailyHistory(historyDoc) {
|
|
389
|
-
const trades = this.getTrades(historyDoc);
|
|
390
|
-
if (!trades.length) return [];
|
|
391
|
-
|
|
392
|
-
const dailyMap = new Map();
|
|
393
|
-
|
|
394
|
-
for (const t of trades) {
|
|
395
|
-
if (!t.CloseDateTime) continue;
|
|
396
|
-
// Extract YYYY-MM-DD
|
|
397
|
-
const date = new Date(t.CloseDateTime).toISOString().slice(0, 10);
|
|
398
|
-
const pnl = t.NetProfit || 0;
|
|
399
|
-
const invested = t.Amount || t.Invested || 0; // Handle schema variance
|
|
400
|
-
|
|
401
|
-
if (!dailyMap.has(date)) {
|
|
402
|
-
dailyMap.set(date, {
|
|
403
|
-
date: date,
|
|
404
|
-
netProfit: 0,
|
|
405
|
-
investedVolume: 0,
|
|
406
|
-
tradesCount: 0
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
const entry = dailyMap.get(date);
|
|
411
|
-
entry.netProfit += pnl;
|
|
412
|
-
entry.investedVolume += invested;
|
|
413
|
-
entry.tradesCount++;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Return sorted array
|
|
417
|
-
return Array.from(dailyMap.values()).sort((a, b) => a.date.localeCompare(b.date));
|
|
418
|
-
}
|
|
419
|
-
// -------------------------
|
|
420
|
-
|
|
421
|
-
static getTradedAssets(historyDoc) {
|
|
422
|
-
const trades = this.getTrades(historyDoc);
|
|
423
|
-
if (!trades.length) return [];
|
|
424
|
-
|
|
425
|
-
const assetsMap = new Map();
|
|
426
|
-
for (const t of trades) {
|
|
427
|
-
const instId = t.InstrumentID;
|
|
428
|
-
if (!instId) continue;
|
|
429
|
-
|
|
430
|
-
if (!assetsMap.has(instId)) {
|
|
431
|
-
assetsMap.set(instId, { instrumentId: instId, totalDuration: 0, count: 0 });
|
|
432
|
-
}
|
|
433
|
-
const asset = assetsMap.get(instId);
|
|
434
|
-
|
|
435
|
-
if (t.OpenDateTime && t.CloseDateTime) {
|
|
436
|
-
const open = new Date(t.OpenDateTime);
|
|
437
|
-
const close = new Date(t.CloseDateTime);
|
|
438
|
-
const durationMins = (close - open) / 60000;
|
|
439
|
-
if (durationMins > 0) {
|
|
440
|
-
asset.totalDuration += durationMins;
|
|
441
|
-
asset.count++;
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
return Array.from(assetsMap.values()).map(a => ({
|
|
447
|
-
instrumentId: a.instrumentId,
|
|
448
|
-
avgHoldingTimeInMinutes: a.count > 0 ? (a.totalDuration / a.count) : 0
|
|
449
|
-
}));
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
static getSummary(historyDoc) {
|
|
453
|
-
const trades = this.getTrades(historyDoc);
|
|
454
|
-
if (!trades.length) return null;
|
|
455
|
-
|
|
456
|
-
let totalTrades = trades.length;
|
|
457
|
-
let wins = 0;
|
|
458
|
-
let totalProf = 0;
|
|
459
|
-
let totalLoss = 0;
|
|
460
|
-
let profCount = 0;
|
|
461
|
-
let lossCount = 0;
|
|
462
|
-
let totalDur = 0;
|
|
463
|
-
let validDurCount = 0;
|
|
464
|
-
|
|
465
|
-
for (const t of trades) {
|
|
466
|
-
const netProfit = t.NetProfit || 0;
|
|
467
|
-
|
|
468
|
-
if (netProfit > 0) {
|
|
469
|
-
wins++;
|
|
470
|
-
totalProf += netProfit;
|
|
471
|
-
profCount++;
|
|
472
|
-
} else if (netProfit < 0) {
|
|
473
|
-
totalLoss += netProfit;
|
|
474
|
-
lossCount++;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
if (t.OpenDateTime && t.CloseDateTime) {
|
|
478
|
-
const open = new Date(t.OpenDateTime);
|
|
479
|
-
const close = new Date(t.CloseDateTime);
|
|
480
|
-
totalDur += (close - open) / 60000;
|
|
481
|
-
validDurCount++;
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
return {
|
|
486
|
-
totalTrades: totalTrades,
|
|
487
|
-
winRatio: totalTrades > 0 ? (wins / totalTrades) * 100 : 0,
|
|
488
|
-
avgProfit: profCount > 0 ? totalProf / profCount : 0,
|
|
489
|
-
avgLoss: lossCount > 0 ? totalLoss / lossCount : 0,
|
|
490
|
-
avgHoldingTimeInMinutes: validDurCount > 0 ? totalDur / validDurCount : 0
|
|
491
|
-
};
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
/**
|
|
495
|
-
* Extract sectors from historical trades.
|
|
496
|
-
* @param {Object} historyDoc - Trade history document
|
|
497
|
-
* @param {Object} mappings - Context mappings containing instrumentToSector
|
|
498
|
-
* @returns {string[]} Array of unique sector names
|
|
499
|
-
*/
|
|
500
|
-
static getTradedSectors(historyDoc, mappings) {
|
|
501
|
-
const trades = this.getTrades(historyDoc);
|
|
502
|
-
const sectors = new Set();
|
|
503
|
-
const sectorMap = mappings?.instrumentToSector || {};
|
|
504
|
-
|
|
505
|
-
for (const trade of trades) {
|
|
506
|
-
const instId = trade.InstrumentID;
|
|
507
|
-
if (instId) {
|
|
508
|
-
const sector = sectorMap[instId] || 'Unknown';
|
|
509
|
-
sectors.add(sector);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
return Array.from(sectors);
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
/**
|
|
516
|
-
* Extract list of Popular Investors that user has copied (from trade history).
|
|
517
|
-
* @param {Object} historyDoc - Trade history document
|
|
518
|
-
* @returns {number[]} Array of ParentCID values
|
|
519
|
-
*/
|
|
520
|
-
static getCopiedUsers(historyDoc) {
|
|
521
|
-
const trades = this.getTrades(historyDoc);
|
|
522
|
-
const copiedUsers = new Set();
|
|
523
|
-
|
|
524
|
-
for (const trade of trades) {
|
|
525
|
-
if (trade.ParentCID && trade.ParentCID > 0) {
|
|
526
|
-
copiedUsers.add(trade.ParentCID);
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
return Array.from(copiedUsers);
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
class InsightsExtractor {
|
|
534
|
-
static getInsights(context, timeframe = 'today') {
|
|
535
|
-
const insightsData = context.insights;
|
|
536
|
-
if (!insightsData) return [];
|
|
537
|
-
const doc = insightsData[timeframe];
|
|
538
|
-
if (!doc) return [];
|
|
539
|
-
if (doc.insights && Array.isArray(doc.insights)) { return doc.insights; }
|
|
540
|
-
return [];
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
static getInsightForInstrument(insights, instrumentId) {
|
|
544
|
-
if (!insights || !Array.isArray(insights)) return null;
|
|
545
|
-
return insights.find(i => i.instrumentId === instrumentId) || null;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
static getTotalOwners(insight) { return insight ? (insight.total || 0) : 0; }
|
|
549
|
-
static getLongPercent(insight) { return insight ? (insight.buy || 0) : 0; }
|
|
550
|
-
static getShortPercent(insight) { return insight ? (insight.sell || 0) : 0; }
|
|
551
|
-
static getGrowthPercent(insight) { return insight ? (insight.growth || 0) : 0; }
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
/**
|
|
555
|
-
* [NEW] Extractor for PI Ratings Data
|
|
556
|
-
* Access via: context.globalData.ratings
|
|
557
|
-
* Schema: { piCid: { averageRating, totalRatings, ratingsByUser: { userId: rating } } }
|
|
558
|
-
*/
|
|
559
|
-
class RatingsExtractor {
|
|
560
|
-
/**
|
|
561
|
-
* Get ratings data for a specific PI
|
|
562
|
-
* @param {Object} ratingsData - The ratings data from context.globalData.ratings
|
|
563
|
-
* @param {string|number} piCid - Popular Investor CID
|
|
564
|
-
* @returns {Object|null} PI ratings data or null if not found
|
|
565
|
-
*/
|
|
566
|
-
static getPIRatings(ratingsData, piCid) {
|
|
567
|
-
if (!ratingsData || !piCid) return null;
|
|
568
|
-
return ratingsData[String(piCid)] || null;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
/**
|
|
572
|
-
* Get average rating for a PI
|
|
573
|
-
* @param {Object} ratingsData - The ratings data from context.globalData.ratings
|
|
574
|
-
* @param {string|number} piCid - Popular Investor CID
|
|
575
|
-
* @returns {number} Average rating (0-5) or 0 if not available
|
|
576
|
-
*/
|
|
577
|
-
static getAverageRating(ratingsData, piCid) {
|
|
578
|
-
const piRatings = this.getPIRatings(ratingsData, piCid);
|
|
579
|
-
return piRatings?.averageRating || 0;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
/**
|
|
583
|
-
* Get total number of ratings for a PI
|
|
584
|
-
* @param {Object} ratingsData - The ratings data from context.globalData.ratings
|
|
585
|
-
* @param {string|number} piCid - Popular Investor CID
|
|
586
|
-
* @returns {number} Total ratings count or 0 if not available
|
|
587
|
-
*/
|
|
588
|
-
static getTotalRatings(ratingsData, piCid) {
|
|
589
|
-
const piRatings = this.getPIRatings(ratingsData, piCid);
|
|
590
|
-
return piRatings?.totalRatings || 0;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
/**
|
|
594
|
-
* Get ratings by user map for a PI
|
|
595
|
-
* @param {Object} ratingsData - The ratings data from context.globalData.ratings
|
|
596
|
-
* @param {string|number} piCid - Popular Investor CID
|
|
597
|
-
* @returns {Object} Map of userId -> rating, or empty object if not available
|
|
598
|
-
*/
|
|
599
|
-
static getRatingsByUser(ratingsData, piCid) {
|
|
600
|
-
const piRatings = this.getPIRatings(ratingsData, piCid);
|
|
601
|
-
return piRatings?.ratingsByUser || {};
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
/**
|
|
605
|
-
* Get a specific user's rating for a PI
|
|
606
|
-
* @param {Object} ratingsData - The ratings data from context.globalData.ratings
|
|
607
|
-
* @param {string|number} piCid - Popular Investor CID
|
|
608
|
-
* @param {string|number} userId - User CID
|
|
609
|
-
* @returns {number|null} User's rating (1-5) or null if not found
|
|
610
|
-
*/
|
|
611
|
-
static getUserRating(ratingsData, piCid, userId) {
|
|
612
|
-
const ratingsByUser = this.getRatingsByUser(ratingsData, piCid);
|
|
613
|
-
const rating = ratingsByUser[String(userId)];
|
|
614
|
-
return rating !== undefined ? rating : null;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
/**
|
|
618
|
-
* Get all PIs with ratings data
|
|
619
|
-
* @param {Object} ratingsData - The ratings data from context.globalData.ratings
|
|
620
|
-
* @returns {string[]} Array of PI CIDs that have ratings
|
|
621
|
-
*/
|
|
622
|
-
static getAllPIs(ratingsData) {
|
|
623
|
-
if (!ratingsData || typeof ratingsData !== 'object') return [];
|
|
624
|
-
return Object.keys(ratingsData).filter(key => key !== 'date' && key !== 'lastUpdated');
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
/**
|
|
629
|
-
* [NEW] Extractor for PI Page Views Data
|
|
630
|
-
* Access via: context.globalData.pageViews
|
|
631
|
-
* Schema: { piCid: { totalViews, uniqueViewers, viewsByUser: { userId: { viewCount, lastViewed } } } }
|
|
632
|
-
*/
|
|
633
|
-
class PageViewsExtractor {
|
|
634
|
-
/**
|
|
635
|
-
* Get page views data for a specific PI
|
|
636
|
-
* @param {Object} pageViewsData - The page views data from context.globalData.pageViews
|
|
637
|
-
* @param {string|number} piCid - Popular Investor CID
|
|
638
|
-
* @returns {Object|null} PI page views data or null if not found
|
|
639
|
-
*/
|
|
640
|
-
static getPIPageViews(pageViewsData, piCid) {
|
|
641
|
-
if (!pageViewsData || !piCid) return null;
|
|
642
|
-
return pageViewsData[String(piCid)] || null;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
/**
|
|
646
|
-
* Get total page views for a PI
|
|
647
|
-
* @param {Object} pageViewsData - The page views data from context.globalData.pageViews
|
|
648
|
-
* @param {string|number} piCid - Popular Investor CID
|
|
649
|
-
* @returns {number} Total views count or 0 if not available
|
|
650
|
-
*/
|
|
651
|
-
static getTotalViews(pageViewsData, piCid) {
|
|
652
|
-
const piViews = this.getPIPageViews(pageViewsData, piCid);
|
|
653
|
-
return piViews?.totalViews || 0;
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
/**
|
|
657
|
-
* Get unique viewers count for a PI
|
|
658
|
-
* @param {Object} pageViewsData - The page views data from context.globalData.pageViews
|
|
659
|
-
* @param {string|number} piCid - Popular Investor CID
|
|
660
|
-
* @returns {number} Unique viewers count or 0 if not available
|
|
661
|
-
*/
|
|
662
|
-
static getUniqueViewers(pageViewsData, piCid) {
|
|
663
|
-
const piViews = this.getPIPageViews(pageViewsData, piCid);
|
|
664
|
-
return piViews?.uniqueViewers || 0;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
/**
|
|
668
|
-
* Get views by user map for a PI
|
|
669
|
-
* @param {Object} pageViewsData - The page views data from context.globalData.pageViews
|
|
670
|
-
* @param {string|number} piCid - Popular Investor CID
|
|
671
|
-
* @returns {Object} Map of userId -> { viewCount, lastViewed }, or empty object if not available
|
|
672
|
-
*/
|
|
673
|
-
static getViewsByUser(pageViewsData, piCid) {
|
|
674
|
-
const piViews = this.getPIPageViews(pageViewsData, piCid);
|
|
675
|
-
return piViews?.viewsByUser || {};
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
/**
|
|
679
|
-
* Get a specific user's view data for a PI
|
|
680
|
-
* @param {Object} pageViewsData - The page views data from context.globalData.pageViews
|
|
681
|
-
* @param {string|number} piCid - Popular Investor CID
|
|
682
|
-
* @param {string|number} userId - User CID
|
|
683
|
-
* @returns {Object|null} User's view data { viewCount, lastViewed } or null if not found
|
|
684
|
-
*/
|
|
685
|
-
static getUserViews(pageViewsData, piCid, userId) {
|
|
686
|
-
const viewsByUser = this.getViewsByUser(pageViewsData, piCid);
|
|
687
|
-
return viewsByUser[String(userId)] || null;
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
/**
|
|
691
|
-
* Get view count for a specific user
|
|
692
|
-
* @param {Object} pageViewsData - The page views data from context.globalData.pageViews
|
|
693
|
-
* @param {string|number} piCid - Popular Investor CID
|
|
694
|
-
* @param {string|number} userId - User CID
|
|
695
|
-
* @returns {number} User's view count or 0 if not found
|
|
696
|
-
*/
|
|
697
|
-
static getUserViewCount(pageViewsData, piCid, userId) {
|
|
698
|
-
const userViews = this.getUserViews(pageViewsData, piCid, userId);
|
|
699
|
-
return userViews?.viewCount || 0;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
/**
|
|
703
|
-
* Get last viewed timestamp for a specific user
|
|
704
|
-
* @param {Object} pageViewsData - The page views data from context.globalData.pageViews
|
|
705
|
-
* @param {string|number} piCid - Popular Investor CID
|
|
706
|
-
* @param {string|number} userId - User CID
|
|
707
|
-
* @returns {Date|null} Last viewed date or null if not found
|
|
708
|
-
*/
|
|
709
|
-
static getUserLastViewed(pageViewsData, piCid, userId) {
|
|
710
|
-
const userViews = this.getUserViews(pageViewsData, piCid, userId);
|
|
711
|
-
if (!userViews || !userViews.lastViewed) return null;
|
|
712
|
-
return userViews.lastViewed.toDate ? userViews.lastViewed.toDate() : new Date(userViews.lastViewed);
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
/**
|
|
716
|
-
* Get all PIs with page views data
|
|
717
|
-
* @param {Object} pageViewsData - The page views data from context.globalData.pageViews
|
|
718
|
-
* @returns {string[]} Array of PI CIDs that have page views
|
|
719
|
-
*/
|
|
720
|
-
static getAllPIs(pageViewsData) {
|
|
721
|
-
if (!pageViewsData || typeof pageViewsData !== 'object') return [];
|
|
722
|
-
return Object.keys(pageViewsData).filter(key => key !== 'date' && key !== 'lastUpdated');
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
/**
|
|
727
|
-
* [NEW] Extractor for Watchlist Membership Data
|
|
728
|
-
* Access via: context.globalData.watchlistMembership
|
|
729
|
-
* Schema: { piCid: { totalUsers, users: [userId], publicWatchlistCount, privateWatchlistCount } }
|
|
730
|
-
*/
|
|
731
|
-
class WatchlistMembershipExtractor {
|
|
732
|
-
/**
|
|
733
|
-
* Get watchlist membership data for a specific PI
|
|
734
|
-
* @param {Object} watchlistData - The watchlist data from context.globalData.watchlistMembership
|
|
735
|
-
* @param {string|number} piCid - Popular Investor CID
|
|
736
|
-
* @returns {Object|null} PI watchlist membership data or null if not found
|
|
737
|
-
*/
|
|
738
|
-
static getPIMembership(watchlistData, piCid) {
|
|
739
|
-
if (!watchlistData || !piCid) return null;
|
|
740
|
-
return watchlistData[String(piCid)] || null;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
/**
|
|
744
|
-
* Get total users who have this PI in their watchlist
|
|
745
|
-
* @param {Object} watchlistData - The watchlist data from context.globalData.watchlistMembership
|
|
746
|
-
* @param {string|number} piCid - Popular Investor CID
|
|
747
|
-
* @returns {number} Total users count or 0 if not available
|
|
748
|
-
*/
|
|
749
|
-
static getTotalUsers(watchlistData, piCid) {
|
|
750
|
-
const membership = this.getPIMembership(watchlistData, piCid);
|
|
751
|
-
return membership?.totalUsers || 0;
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
/**
|
|
755
|
-
* Get array of user CIDs who have this PI in their watchlist
|
|
756
|
-
* @param {Object} watchlistData - The watchlist data from context.globalData.watchlistMembership
|
|
757
|
-
* @param {string|number} piCid - Popular Investor CID
|
|
758
|
-
* @returns {string[]} Array of user CIDs, or empty array if not available
|
|
759
|
-
*/
|
|
760
|
-
static getUsers(watchlistData, piCid) {
|
|
761
|
-
const membership = this.getPIMembership(watchlistData, piCid);
|
|
762
|
-
if (!membership || !Array.isArray(membership.users)) return [];
|
|
763
|
-
return membership.users.map(u => String(u));
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
/**
|
|
767
|
-
* Check if a specific user has this PI in their watchlist
|
|
768
|
-
* @param {Object} watchlistData - The watchlist data from context.globalData.watchlistMembership
|
|
769
|
-
* @param {string|number} piCid - Popular Investor CID
|
|
770
|
-
* @param {string|number} userId - User CID
|
|
771
|
-
* @returns {boolean} True if user has PI in watchlist
|
|
772
|
-
*/
|
|
773
|
-
static hasUser(watchlistData, piCid, userId) {
|
|
774
|
-
const users = this.getUsers(watchlistData, piCid);
|
|
775
|
-
return users.includes(String(userId));
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
/**
|
|
779
|
-
* Get public watchlist count for a PI
|
|
780
|
-
* @param {Object} watchlistData - The watchlist data from context.globalData.watchlistMembership
|
|
781
|
-
* @param {string|number} piCid - Popular Investor CID
|
|
782
|
-
* @returns {number} Public watchlist count or 0 if not available
|
|
783
|
-
*/
|
|
784
|
-
static getPublicWatchlistCount(watchlistData, piCid) {
|
|
785
|
-
const membership = this.getPIMembership(watchlistData, piCid);
|
|
786
|
-
return membership?.publicWatchlistCount || 0;
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
/**
|
|
790
|
-
* Get private watchlist count for a PI
|
|
791
|
-
* @param {Object} watchlistData - The watchlist data from context.globalData.watchlistMembership
|
|
792
|
-
* @param {string|number} piCid - Popular Investor CID
|
|
793
|
-
* @returns {number} Private watchlist count or 0 if not available
|
|
794
|
-
*/
|
|
795
|
-
static getPrivateWatchlistCount(watchlistData, piCid) {
|
|
796
|
-
const membership = this.getPIMembership(watchlistData, piCid);
|
|
797
|
-
return membership?.privateWatchlistCount || 0;
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
/**
|
|
801
|
-
* Get all PIs with watchlist membership data
|
|
802
|
-
* @param {Object} watchlistData - The watchlist data from context.globalData.watchlistMembership
|
|
803
|
-
* @returns {string[]} Array of PI CIDs that have watchlist membership
|
|
804
|
-
*/
|
|
805
|
-
static getAllPIs(watchlistData) {
|
|
806
|
-
if (!watchlistData || typeof watchlistData !== 'object') return [];
|
|
807
|
-
return Object.keys(watchlistData).filter(key => key !== 'date' && key !== 'lastUpdated');
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
/**
|
|
812
|
-
* [NEW] Extractor for PI-Centric Watchlist Data
|
|
813
|
-
* Access via: loader.loadPIWatchlistData(piCid) or context-specific loading
|
|
814
|
-
* Schema: { totalUsers, userCids: [], dailyAdditions: { date: { count, userCids: [], timestamp } }, lastUpdated }
|
|
815
|
-
*/
|
|
816
|
-
class PIWatchlistDataExtractor {
|
|
817
|
-
/**
|
|
818
|
-
* Get total users who have this PI in their watchlist
|
|
819
|
-
* @param {Object} piWatchlistData - The PI watchlist data from PopularInvestors/{piCid}/watchlistData/current
|
|
820
|
-
* @returns {number} Total users count or 0 if not available
|
|
821
|
-
*/
|
|
822
|
-
static getTotalUsers(piWatchlistData) {
|
|
823
|
-
if (!piWatchlistData) return 0;
|
|
824
|
-
return piWatchlistData.totalUsers || 0;
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
/**
|
|
828
|
-
* Get array of user CIDs who have this PI in their watchlist
|
|
829
|
-
* @param {Object} piWatchlistData - The PI watchlist data from PopularInvestors/{piCid}/watchlistData/current
|
|
830
|
-
* @returns {string[]} Array of user CIDs, or empty array if not available
|
|
831
|
-
*/
|
|
832
|
-
static getUserCids(piWatchlistData) {
|
|
833
|
-
if (!piWatchlistData || !Array.isArray(piWatchlistData.userCids)) return [];
|
|
834
|
-
return piWatchlistData.userCids.map(cid => String(cid));
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
/**
|
|
838
|
-
* Check if a specific user has this PI in their watchlist
|
|
839
|
-
* @param {Object} piWatchlistData - The PI watchlist data from PopularInvestors/{piCid}/watchlistData/current
|
|
840
|
-
* @param {string|number} userId - User CID
|
|
841
|
-
* @returns {boolean} True if user has PI in watchlist
|
|
842
|
-
*/
|
|
843
|
-
static hasUser(piWatchlistData, userId) {
|
|
844
|
-
const userCids = this.getUserCids(piWatchlistData);
|
|
845
|
-
return userCids.includes(String(userId));
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
/**
|
|
849
|
-
* Get daily additions data for a specific date
|
|
850
|
-
* @param {Object} piWatchlistData - The PI watchlist data from PopularInvestors/{piCid}/watchlistData/current
|
|
851
|
-
* @param {string} date - Date string in YYYY-MM-DD format
|
|
852
|
-
* @returns {Object|null} Daily addition data { count, userCids: [], timestamp } or null if not found
|
|
853
|
-
*/
|
|
854
|
-
static getDailyAdditions(piWatchlistData, date) {
|
|
855
|
-
if (!piWatchlistData || !piWatchlistData.dailyAdditions || typeof piWatchlistData.dailyAdditions !== 'object') {
|
|
856
|
-
return null;
|
|
857
|
-
}
|
|
858
|
-
return piWatchlistData.dailyAdditions[date] || null;
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
/**
|
|
862
|
-
* Get count of users who added this PI to their watchlist on a specific date
|
|
863
|
-
* @param {Object} piWatchlistData - The PI watchlist data from PopularInvestors/{piCid}/watchlistData/current
|
|
864
|
-
* @param {string} date - Date string in YYYY-MM-DD format
|
|
865
|
-
* @returns {number} Count of users who added on this date, or 0 if not available
|
|
866
|
-
*/
|
|
867
|
-
static getDailyAdditionCount(piWatchlistData, date) {
|
|
868
|
-
const dailyData = this.getDailyAdditions(piWatchlistData, date);
|
|
869
|
-
return dailyData?.count || 0;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
/**
|
|
873
|
-
* Get user CIDs who added this PI to their watchlist on a specific date
|
|
874
|
-
* @param {Object} piWatchlistData - The PI watchlist data from PopularInvestors/{piCid}/watchlistData/current
|
|
875
|
-
* @param {string} date - Date string in YYYY-MM-DD format
|
|
876
|
-
* @returns {string[]} Array of user CIDs who added on this date, or empty array if not available
|
|
877
|
-
*/
|
|
878
|
-
static getDailyAdditionUserCids(piWatchlistData, date) {
|
|
879
|
-
const dailyData = this.getDailyAdditions(piWatchlistData, date);
|
|
880
|
-
if (!dailyData || !Array.isArray(dailyData.userCids)) return [];
|
|
881
|
-
return dailyData.userCids.map(cid => String(cid));
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
/**
|
|
885
|
-
* Get all dates with daily addition data
|
|
886
|
-
* @param {Object} piWatchlistData - The PI watchlist data from PopularInvestors/{piCid}/watchlistData/current
|
|
887
|
-
* @returns {string[]} Array of date strings (YYYY-MM-DD format), or empty array if not available
|
|
888
|
-
*/
|
|
889
|
-
static getAllDates(piWatchlistData) {
|
|
890
|
-
if (!piWatchlistData || !piWatchlistData.dailyAdditions || typeof piWatchlistData.dailyAdditions !== 'object') {
|
|
891
|
-
return [];
|
|
892
|
-
}
|
|
893
|
-
return Object.keys(piWatchlistData.dailyAdditions);
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
/**
|
|
897
|
-
* Get last updated timestamp
|
|
898
|
-
* @param {Object} piWatchlistData - The PI watchlist data from PopularInvestors/{piCid}/watchlistData/current
|
|
899
|
-
* @returns {Object|null} Timestamp object or null if not available
|
|
900
|
-
*/
|
|
901
|
-
static getLastUpdated(piWatchlistData) {
|
|
902
|
-
if (!piWatchlistData) return null;
|
|
903
|
-
return piWatchlistData.lastUpdated || null;
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
/**
|
|
908
|
-
* [NEW] Extractor for PI Alert History Data
|
|
909
|
-
* Access via: context.globalData.alertHistory
|
|
910
|
-
* Schema: { piCid: { alertType: { triggered, count, triggeredFor: [userId], metadata: {...}, lastTriggered } } }
|
|
911
|
-
*/
|
|
912
|
-
class AlertHistoryExtractor {
|
|
913
|
-
/**
|
|
914
|
-
* Get alert history data for a specific PI
|
|
915
|
-
* @param {Object} alertHistoryData - The alert history data from context.globalData.alertHistory
|
|
916
|
-
* @param {string|number} piCid - Popular Investor CID
|
|
917
|
-
* @returns {Object|null} PI alert history data or null if not found
|
|
918
|
-
*/
|
|
919
|
-
static getPIAlertHistory(alertHistoryData, piCid) {
|
|
920
|
-
if (!alertHistoryData || !piCid) return null;
|
|
921
|
-
return alertHistoryData[String(piCid)] || null;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
/**
|
|
925
|
-
* Get all alert types for a PI
|
|
926
|
-
* @param {Object} alertHistoryData - The alert history data from context.globalData.alertHistory
|
|
927
|
-
* @param {string|number} piCid - Popular Investor CID
|
|
928
|
-
* @returns {string[]} Array of alert type names, or empty array if not available
|
|
929
|
-
*/
|
|
930
|
-
static getAlertTypes(alertHistoryData, piCid) {
|
|
931
|
-
const piAlerts = this.getPIAlertHistory(alertHistoryData, piCid);
|
|
932
|
-
if (!piAlerts || typeof piAlerts !== 'object') return [];
|
|
933
|
-
return Object.keys(piAlerts).filter(key => key !== 'date' && key !== 'lastUpdated');
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
/**
|
|
937
|
-
* Get alert data for a specific alert type
|
|
938
|
-
* @param {Object} alertHistoryData - The alert history data from context.globalData.alertHistory
|
|
939
|
-
* @param {string|number} piCid - Popular Investor CID
|
|
940
|
-
* @param {string} alertType - Alert type name (e.g., "RiskScoreIncrease")
|
|
941
|
-
* @returns {Object|null} Alert data or null if not found
|
|
942
|
-
*/
|
|
943
|
-
static getAlertData(alertHistoryData, piCid, alertType) {
|
|
944
|
-
const piAlerts = this.getPIAlertHistory(alertHistoryData, piCid);
|
|
945
|
-
if (!piAlerts || !alertType) return null;
|
|
946
|
-
return piAlerts[alertType] || null;
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
/**
|
|
950
|
-
* Check if an alert type was triggered for a PI
|
|
951
|
-
* @param {Object} alertHistoryData - The alert history data from context.globalData.alertHistory
|
|
952
|
-
* @param {string|number} piCid - Popular Investor CID
|
|
953
|
-
* @param {string} alertType - Alert type name
|
|
954
|
-
* @returns {boolean} True if alert was triggered
|
|
955
|
-
*/
|
|
956
|
-
static isTriggered(alertHistoryData, piCid, alertType) {
|
|
957
|
-
const alertData = this.getAlertData(alertHistoryData, piCid, alertType);
|
|
958
|
-
return alertData?.triggered === true;
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
/**
|
|
962
|
-
* Get trigger count for an alert type
|
|
963
|
-
* @param {Object} alertHistoryData - The alert history data from context.globalData.alertHistory
|
|
964
|
-
* @param {string|number} piCid - Popular Investor CID
|
|
965
|
-
* @param {string} alertType - Alert type name
|
|
966
|
-
* @returns {number} Trigger count or 0 if not available
|
|
967
|
-
*/
|
|
968
|
-
static getTriggerCount(alertHistoryData, piCid, alertType) {
|
|
969
|
-
const alertData = this.getAlertData(alertHistoryData, piCid, alertType);
|
|
970
|
-
return alertData?.count || 0;
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
/**
|
|
974
|
-
* Get array of user CIDs for whom the alert was triggered
|
|
975
|
-
* @param {Object} alertHistoryData - The alert history data from context.globalData.alertHistory
|
|
976
|
-
* @param {string|number} piCid - Popular Investor CID
|
|
977
|
-
* @param {string} alertType - Alert type name
|
|
978
|
-
* @returns {string[]} Array of user CIDs, or empty array if not available
|
|
979
|
-
*/
|
|
980
|
-
static getTriggeredFor(alertHistoryData, piCid, alertType) {
|
|
981
|
-
const alertData = this.getAlertData(alertHistoryData, piCid, alertType);
|
|
982
|
-
if (!alertData || !Array.isArray(alertData.triggeredFor)) return [];
|
|
983
|
-
return alertData.triggeredFor.map(u => String(u));
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
/**
|
|
987
|
-
* Check if alert was triggered for a specific user
|
|
988
|
-
* @param {Object} alertHistoryData - The alert history data from context.globalData.alertHistory
|
|
989
|
-
* @param {string|number} piCid - Popular Investor CID
|
|
990
|
-
* @param {string} alertType - Alert type name
|
|
991
|
-
* @param {string|number} userId - User CID
|
|
992
|
-
* @returns {boolean} True if alert was triggered for this user
|
|
993
|
-
*/
|
|
994
|
-
static isTriggeredForUser(alertHistoryData, piCid, alertType, userId) {
|
|
995
|
-
const triggeredFor = this.getTriggeredFor(alertHistoryData, piCid, alertType);
|
|
996
|
-
return triggeredFor.includes(String(userId));
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
/**
|
|
1000
|
-
* Get metadata for an alert type
|
|
1001
|
-
* @param {Object} alertHistoryData - The alert history data from context.globalData.alertHistory
|
|
1002
|
-
* @param {string|number} piCid - Popular Investor CID
|
|
1003
|
-
* @param {string} alertType - Alert type name
|
|
1004
|
-
* @returns {Object} Alert metadata or empty object if not available
|
|
1005
|
-
*/
|
|
1006
|
-
static getMetadata(alertHistoryData, piCid, alertType) {
|
|
1007
|
-
const alertData = this.getAlertData(alertHistoryData, piCid, alertType);
|
|
1008
|
-
return alertData?.metadata || {};
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
/**
|
|
1012
|
-
* Get last triggered timestamp for an alert type
|
|
1013
|
-
* @param {Object} alertHistoryData - The alert history data from context.globalData.alertHistory
|
|
1014
|
-
* @param {string|number} piCid - Popular Investor CID
|
|
1015
|
-
* @param {string} alertType - Alert type name
|
|
1016
|
-
* @returns {Date|null} Last triggered date or null if not found
|
|
1017
|
-
*/
|
|
1018
|
-
static getLastTriggered(alertHistoryData, piCid, alertType) {
|
|
1019
|
-
const alertData = this.getAlertData(alertHistoryData, piCid, alertType);
|
|
1020
|
-
if (!alertData || !alertData.lastTriggered) return null;
|
|
1021
|
-
return alertData.lastTriggered.toDate ? alertData.lastTriggered.toDate() : new Date(alertData.lastTriggered);
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
/**
|
|
1025
|
-
* Get all triggered alerts for a PI
|
|
1026
|
-
* @param {Object} alertHistoryData - The alert history data from context.globalData.alertHistory
|
|
1027
|
-
* @param {string|number} piCid - Popular Investor CID
|
|
1028
|
-
* @returns {string[]} Array of alert type names that were triggered
|
|
1029
|
-
*/
|
|
1030
|
-
static getTriggeredAlerts(alertHistoryData, piCid) {
|
|
1031
|
-
const alertTypes = this.getAlertTypes(alertHistoryData, piCid);
|
|
1032
|
-
return alertTypes.filter(type => this.isTriggered(alertHistoryData, piCid, type));
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
/**
|
|
1036
|
-
* Get all PIs with alert history data
|
|
1037
|
-
* @param {Object} alertHistoryData - The alert history data from context.globalData.alertHistory
|
|
1038
|
-
* @returns {string[]} Array of PI CIDs that have alert history
|
|
1039
|
-
*/
|
|
1040
|
-
static getAllPIs(alertHistoryData) {
|
|
1041
|
-
if (!alertHistoryData || typeof alertHistoryData !== 'object') return [];
|
|
1042
|
-
return Object.keys(alertHistoryData).filter(key => key !== 'date' && key !== 'lastUpdated');
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
/**
|
|
1047
|
-
* [NEW] Extractor for Popular Investor Master List (Global Backup)
|
|
1048
|
-
* Access via: context.globalData.piMasterList
|
|
1049
|
-
*/
|
|
1050
|
-
class PIMasterListExtractor {
|
|
1051
|
-
/**
|
|
1052
|
-
* Get the master record for a PI
|
|
1053
|
-
* @param {Object} masterList - context.globalData.piMasterList
|
|
1054
|
-
* @param {string} cid - The User CID
|
|
1055
|
-
*/
|
|
1056
|
-
static getRecord(masterList, cid) {
|
|
1057
|
-
if (!masterList || !cid) return null;
|
|
1058
|
-
return masterList[String(cid)] || null;
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
/**
|
|
1062
|
-
* Resolve Username from Master List
|
|
1063
|
-
* @param {Object} masterList
|
|
1064
|
-
* @param {string} cid
|
|
1065
|
-
*/
|
|
1066
|
-
static getUsername(masterList, cid) {
|
|
1067
|
-
const record = this.getRecord(masterList, cid);
|
|
1068
|
-
return record ? record.username : null;
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
/**
|
|
1072
|
-
* Check if CID exists in master list
|
|
1073
|
-
*/
|
|
1074
|
-
static exists(masterList, cid) {
|
|
1075
|
-
return !!this.getRecord(masterList, cid);
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
module.exports = {
|
|
1080
|
-
DataExtractor,
|
|
1081
|
-
priceExtractor,
|
|
1082
|
-
HistoryExtractor,
|
|
1083
|
-
InsightsExtractor,
|
|
1084
|
-
TradeSeriesBuilder,
|
|
1085
|
-
PopularInvestorExtractor,
|
|
1086
|
-
CopyTradingExtractor,
|
|
1087
|
-
RankingsExtractor,
|
|
1088
|
-
VerificationExtractor,
|
|
1089
|
-
SocialExtractor,
|
|
1090
|
-
// [NEW] New Root Data Extractors
|
|
1091
|
-
RatingsExtractor,
|
|
1092
|
-
PageViewsExtractor,
|
|
1093
|
-
WatchlistMembershipExtractor,
|
|
1094
|
-
PIWatchlistDataExtractor,
|
|
1095
|
-
AlertHistoryExtractor,
|
|
1096
|
-
PIMasterListExtractor
|
|
1097
|
-
};
|