bulltrackers-module 1.0.766 → 1.0.769
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-v2/UserPortfolioMetrics.js +50 -0
- package/functions/computation-system-v2/computations/BehavioralAnomaly.js +559 -227
- package/functions/computation-system-v2/computations/GlobalAumPerAsset30D.js +103 -0
- package/functions/computation-system-v2/computations/NewSectorExposure.js +82 -35
- package/functions/computation-system-v2/computations/NewSocialPost.js +52 -24
- package/functions/computation-system-v2/computations/PIDailyAssetAUM.js +134 -0
- package/functions/computation-system-v2/computations/PiFeatureVectors.js +227 -0
- package/functions/computation-system-v2/computations/PiRecommender.js +359 -0
- package/functions/computation-system-v2/computations/PopularInvestorProfileMetrics.js +354 -641
- package/functions/computation-system-v2/computations/SignedInUserList.js +51 -0
- package/functions/computation-system-v2/computations/SignedInUserMirrorHistory.js +138 -0
- package/functions/computation-system-v2/computations/SignedInUserPIProfileMetrics.js +106 -0
- package/functions/computation-system-v2/computations/SignedInUserProfileMetrics.js +324 -0
- package/functions/computation-system-v2/config/bulltrackers.config.js +40 -126
- package/functions/computation-system-v2/core-api.js +17 -9
- package/functions/computation-system-v2/data_schema_reference.MD +108 -0
- package/functions/computation-system-v2/devtools/builder/builder.js +362 -0
- package/functions/computation-system-v2/devtools/builder/examples/user-metrics.yaml +26 -0
- package/functions/computation-system-v2/devtools/index.js +36 -0
- package/functions/computation-system-v2/devtools/shared/MockDataFactory.js +235 -0
- package/functions/computation-system-v2/devtools/shared/SchemaTemplates.js +475 -0
- package/functions/computation-system-v2/devtools/shared/SystemIntrospector.js +517 -0
- package/functions/computation-system-v2/devtools/shared/index.js +16 -0
- package/functions/computation-system-v2/devtools/simulation/DAGAnalyzer.js +243 -0
- package/functions/computation-system-v2/devtools/simulation/MockDataFetcher.js +306 -0
- package/functions/computation-system-v2/devtools/simulation/MockStorageManager.js +336 -0
- package/functions/computation-system-v2/devtools/simulation/SimulationEngine.js +525 -0
- package/functions/computation-system-v2/devtools/simulation/SimulationServer.js +581 -0
- package/functions/computation-system-v2/devtools/simulation/index.js +17 -0
- package/functions/computation-system-v2/devtools/simulation/simulate.js +324 -0
- package/functions/computation-system-v2/devtools/vscode-computation/package.json +90 -0
- package/functions/computation-system-v2/devtools/vscode-computation/snippets/computation.json +128 -0
- package/functions/computation-system-v2/devtools/vscode-computation/src/extension.ts +401 -0
- package/functions/computation-system-v2/devtools/vscode-computation/src/providers/codeActions.ts +152 -0
- package/functions/computation-system-v2/devtools/vscode-computation/src/providers/completions.ts +207 -0
- package/functions/computation-system-v2/devtools/vscode-computation/src/providers/diagnostics.ts +205 -0
- package/functions/computation-system-v2/devtools/vscode-computation/src/providers/hover.ts +205 -0
- package/functions/computation-system-v2/devtools/vscode-computation/tsconfig.json +22 -0
- package/functions/computation-system-v2/docs/HowToCreateComputations.MD +602 -0
- package/functions/computation-system-v2/framework/core/Manifest.js +9 -16
- package/functions/computation-system-v2/framework/core/RunAnalyzer.js +2 -1
- package/functions/computation-system-v2/framework/data/DataFetcher.js +330 -126
- package/functions/computation-system-v2/framework/data/MaterializedViewManager.js +84 -0
- package/functions/computation-system-v2/framework/data/QueryBuilder.js +38 -38
- package/functions/computation-system-v2/framework/execution/Orchestrator.js +226 -153
- package/functions/computation-system-v2/framework/scheduling/ScheduleValidator.js +17 -19
- package/functions/computation-system-v2/framework/storage/StateRepository.js +32 -2
- package/functions/computation-system-v2/framework/storage/StorageManager.js +111 -83
- package/functions/computation-system-v2/framework/testing/ComputationTester.js +161 -66
- package/functions/computation-system-v2/handlers/dispatcher.js +57 -29
- package/functions/computation-system-v2/legacy/PiAssetRecommender.js.old +115 -0
- package/functions/computation-system-v2/legacy/PiSimilarityMatrix.js +104 -0
- package/functions/computation-system-v2/legacy/PiSimilarityVector.js +71 -0
- package/functions/computation-system-v2/scripts/debug_aggregation.js +25 -0
- package/functions/computation-system-v2/scripts/test-computation-dag.js +109 -0
- package/functions/computation-system-v2/scripts/test-invalidation-scenarios.js +234 -0
- package/functions/task-engine/helpers/data_storage_helpers.js +6 -6
- package/package.json +1 -1
- package/functions/computation-system-v2/computations/PopularInvestorRiskAssessment.js +0 -176
- package/functions/computation-system-v2/computations/PopularInvestorRiskMetrics.js +0 -294
- package/functions/computation-system-v2/computations/UserPortfolioSummary.js +0 -172
- package/functions/computation-system-v2/scripts/migrate-sectors.js +0 -73
- package/functions/computation-system-v2/test/analyze-results.js +0 -238
- package/functions/computation-system-v2/test/other/test-dependency-cascade.js +0 -150
- package/functions/computation-system-v2/test/other/test-dispatcher.js +0 -317
- package/functions/computation-system-v2/test/other/test-framework.js +0 -500
- package/functions/computation-system-v2/test/other/test-real-execution.js +0 -166
- package/functions/computation-system-v2/test/other/test-real-integration.js +0 -194
- package/functions/computation-system-v2/test/other/test-refactor-e2e.js +0 -131
- package/functions/computation-system-v2/test/other/test-results.json +0 -31
- package/functions/computation-system-v2/test/other/test-risk-metrics-computation.js +0 -329
- package/functions/computation-system-v2/test/other/test-scheduler.js +0 -204
- package/functions/computation-system-v2/test/other/test-storage.js +0 -449
- package/functions/computation-system-v2/test/run-pipeline-test.js +0 -554
- package/functions/computation-system-v2/test/test-full-pipeline.js +0 -227
- package/functions/computation-system-v2/test/test-worker-pool.js +0 -266
|
@@ -1,234 +1,162 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Popular Investor Profile Metrics
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Original v1 features preserved:
|
|
7
|
-
* - Profile page metrics for Popular Investors
|
|
8
|
-
* - Social engagement (7-day accumulation)
|
|
9
|
-
* - Profitable positions from trade history
|
|
10
|
-
* - Sector/asset exposure analysis
|
|
11
|
-
* - Rankings, ratings, page views, watchlist data
|
|
12
|
-
* - Alert history with 7-day metrics
|
|
13
|
-
* - Exposure over time (historical continuity)
|
|
14
|
-
*
|
|
15
|
-
* v2 Changes:
|
|
16
|
-
* - Uses `getConfig()` instead of `getMetadata()` + `getDependencies()`
|
|
17
|
-
* - Directly references BigQuery table names in `requires`
|
|
18
|
-
* - Data accessed via `context.data['table_name']`
|
|
19
|
-
* - Simplified series handling through lookback configuration
|
|
2
|
+
* @fileoverview Popular Investor Profile Metrics (V3.7 DATA ACCESS FIX)
|
|
3
|
+
* * FIX: Correctly accesses DataFetcher response maps.
|
|
4
|
+
* (Previously failed because it treated the Map-of-Arrays as a flat list of rows).
|
|
5
|
+
* * FIX: 'getEntityRows' helper now handles both Map lookups and flat fallbacks.
|
|
20
6
|
*/
|
|
21
|
-
|
|
22
7
|
const { Computation } = require('../framework');
|
|
23
8
|
|
|
24
9
|
class PopularInvestorProfileMetrics extends Computation {
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* v2 Configuration - Single source of truth for computation requirements.
|
|
28
|
-
*/
|
|
10
|
+
|
|
29
11
|
static getConfig() {
|
|
30
12
|
return {
|
|
31
13
|
name: 'PopularInvestorProfileMetrics',
|
|
14
|
+
type: 'per-entity',
|
|
32
15
|
category: 'popular_investor',
|
|
16
|
+
isHistorical: true,
|
|
33
17
|
|
|
34
|
-
// Direct table references with lookback configuration
|
|
35
18
|
requires: {
|
|
36
|
-
// Core
|
|
19
|
+
// --- Core Data ---
|
|
37
20
|
'portfolio_snapshots': {
|
|
38
|
-
lookback:
|
|
21
|
+
lookback: 30,
|
|
39
22
|
mandatory: true,
|
|
40
|
-
|
|
23
|
+
fields: ['user_id', 'portfolio_data', 'date']
|
|
41
24
|
},
|
|
42
25
|
'trade_history_snapshots': {
|
|
43
|
-
lookback:
|
|
44
|
-
mandatory:
|
|
45
|
-
|
|
26
|
+
lookback: 30,
|
|
27
|
+
mandatory: false,
|
|
28
|
+
fields: ['user_id', 'history_data', 'date']
|
|
29
|
+
},
|
|
30
|
+
'social_post_snapshots': {
|
|
31
|
+
lookback: 7,
|
|
32
|
+
mandatory: false,
|
|
33
|
+
fields: ['user_id', 'posts_data', 'date']
|
|
46
34
|
},
|
|
47
35
|
|
|
48
|
-
//
|
|
36
|
+
// --- Reference ---
|
|
37
|
+
'pi_master_list': {
|
|
38
|
+
lookback: 1,
|
|
39
|
+
mandatory: false,
|
|
40
|
+
fields: ['cid', 'username']
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
// --- KPI Data ---
|
|
49
44
|
'pi_rankings': {
|
|
50
|
-
lookback: 7,
|
|
51
|
-
mandatory: false
|
|
45
|
+
lookback: 7,
|
|
46
|
+
mandatory: false,
|
|
47
|
+
fields: ['pi_id', 'rankings_data', 'username', 'date']
|
|
52
48
|
},
|
|
53
|
-
|
|
54
|
-
// Ratings - 30-day average
|
|
55
49
|
'pi_ratings': {
|
|
56
50
|
lookback: 30,
|
|
57
|
-
mandatory: false
|
|
51
|
+
mandatory: false,
|
|
52
|
+
fields: ['pi_id', 'reviews', 'ratings_by_user', 'average_rating', 'total_ratings', 'date']
|
|
58
53
|
},
|
|
59
|
-
|
|
60
|
-
// Page views - 7-day accumulation
|
|
61
54
|
'pi_page_views': {
|
|
62
|
-
lookback:
|
|
63
|
-
mandatory: false
|
|
55
|
+
lookback: 14,
|
|
56
|
+
mandatory: false,
|
|
57
|
+
fields: ['pi_id', 'views_by_user', 'total_views', 'unique_viewers', 'date']
|
|
64
58
|
},
|
|
65
|
-
|
|
66
|
-
// Watchlist - 30-day trend
|
|
67
59
|
'watchlist_membership': {
|
|
68
60
|
lookback: 30,
|
|
69
|
-
mandatory: false
|
|
61
|
+
mandatory: false,
|
|
62
|
+
fields: ['pi_id', 'total_users', 'date']
|
|
70
63
|
},
|
|
71
|
-
|
|
72
|
-
// Alert history - 7-day metrics
|
|
73
64
|
'pi_alert_history': {
|
|
74
|
-
lookback: 7,
|
|
75
|
-
mandatory: false
|
|
76
|
-
},
|
|
77
|
-
|
|
78
|
-
// Social posts - 7-day engagement
|
|
79
|
-
'social_post_snapshots': {
|
|
80
65
|
lookback: 7,
|
|
81
66
|
mandatory: false,
|
|
82
|
-
|
|
67
|
+
fields: ['pi_id', 'alert_type', 'trigger_count', 'metadata', 'date']
|
|
83
68
|
},
|
|
84
|
-
|
|
85
|
-
//
|
|
86
|
-
'ticker_mappings': {
|
|
87
|
-
|
|
88
|
-
|
|
69
|
+
|
|
70
|
+
// --- Mappings ---
|
|
71
|
+
'ticker_mappings': { mandatory: false },
|
|
72
|
+
'sector_mappings': { mandatory: false }
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
storage: {
|
|
76
|
+
bigquery: true,
|
|
77
|
+
firestore: {
|
|
78
|
+
enabled: true,
|
|
79
|
+
path: 'profiles/{entityId}/metrics/{date}',
|
|
80
|
+
merge: true
|
|
89
81
|
}
|
|
90
82
|
},
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
dependencies: [],
|
|
94
|
-
|
|
95
|
-
// Runs per user
|
|
96
|
-
type: 'per-entity',
|
|
97
|
-
|
|
98
|
-
// Needs previous day's result for exposure over time
|
|
99
|
-
isHistorical: true,
|
|
100
|
-
|
|
101
|
-
// Output config
|
|
102
|
-
ttlDays: 60
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
static getSchema() {
|
|
107
|
-
return {
|
|
108
|
-
type: 'object',
|
|
109
|
-
properties: {
|
|
110
|
-
username: { type: 'string' },
|
|
111
|
-
socialEngagement: { type: 'object' },
|
|
112
|
-
profitablePositions: { type: 'object' },
|
|
113
|
-
topWinningPositions: { type: 'object' },
|
|
114
|
-
sectorPerformance: { type: 'object' },
|
|
115
|
-
sectorExposure: { type: 'object' },
|
|
116
|
-
assetExposure: { type: 'object' },
|
|
117
|
-
portfolioSummary: { type: 'object' },
|
|
118
|
-
rankingsData: { type: 'object' },
|
|
119
|
-
ratingsData: { type: 'object' },
|
|
120
|
-
pageViewsData: { type: 'object' },
|
|
121
|
-
watchlistData: { type: 'object' },
|
|
122
|
-
alertHistoryData: { type: 'object' },
|
|
123
|
-
sectorExposureOverTime: { type: 'object' },
|
|
124
|
-
assetExposureOverTime: { type: 'object' },
|
|
125
|
-
alertMetrics: { type: 'object' }
|
|
126
|
-
}
|
|
83
|
+
|
|
84
|
+
userType: 'POPULAR_INVESTOR'
|
|
127
85
|
};
|
|
128
86
|
}
|
|
129
|
-
|
|
130
|
-
static getWeight() {
|
|
131
|
-
return 10.0; // Complex per-user computation
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Process a single user's profile metrics.
|
|
136
|
-
*/
|
|
87
|
+
|
|
137
88
|
async process(context) {
|
|
138
|
-
const {
|
|
139
|
-
|
|
140
|
-
// Extract data from context
|
|
141
|
-
const portfolio = data['portfolio_snapshots'];
|
|
142
|
-
const historyDoc = data['trade_history_snapshots'];
|
|
143
|
-
const rankings = data['pi_rankings'];
|
|
144
|
-
const ratings = data['pi_ratings'];
|
|
145
|
-
const pageViews = data['pi_page_views'];
|
|
146
|
-
const watchlist = data['watchlist_membership'];
|
|
147
|
-
const alerts = data['pi_alert_history'];
|
|
148
|
-
const social = data['social_post_snapshots'];
|
|
149
|
-
const tickerMappings = data['ticker_mappings'];
|
|
150
|
-
|
|
151
|
-
// Build instrument mappings
|
|
152
|
-
const mappings = this._buildMappings(tickerMappings);
|
|
153
|
-
|
|
154
|
-
// Initialize result structure
|
|
155
|
-
const result = this._initializeResult();
|
|
156
|
-
|
|
157
|
-
// ==========================================================================================
|
|
158
|
-
// 1. Social Engagement (7-Day Accumulation + Chart)
|
|
159
|
-
// ==========================================================================================
|
|
160
|
-
this._processSocialEngagement(result, social, userId);
|
|
161
|
-
|
|
162
|
-
// ==========================================================================================
|
|
163
|
-
// 2. Profitable Positions (From Trade History)
|
|
164
|
-
// ==========================================================================================
|
|
165
|
-
this._processProfitablePositions(result, historyDoc);
|
|
166
|
-
|
|
167
|
-
// ==========================================================================================
|
|
168
|
-
// 3. Top Winning Positions
|
|
169
|
-
// ==========================================================================================
|
|
170
|
-
this._processTopWinningPositions(result, portfolio, historyDoc, mappings);
|
|
171
|
-
|
|
172
|
-
// ==========================================================================================
|
|
173
|
-
// 4. Sector Performance
|
|
174
|
-
// ==========================================================================================
|
|
175
|
-
this._processSectorPerformance(result, portfolio, mappings);
|
|
176
|
-
|
|
177
|
-
// ==========================================================================================
|
|
178
|
-
// 5. & 6. Exposure & Portfolio Summary
|
|
179
|
-
// ==========================================================================================
|
|
180
|
-
this._processExposureAndSummary(result, portfolio, mappings);
|
|
181
|
-
|
|
89
|
+
const { data, entityId, date, rules } = context;
|
|
90
|
+
|
|
182
91
|
// ==========================================================================================
|
|
183
|
-
//
|
|
92
|
+
// 0. PREPARATION & HELPER FUNCTIONS
|
|
184
93
|
// ==========================================================================================
|
|
185
|
-
this._processRankingsData(result, rankings, userId);
|
|
186
94
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
95
|
+
const toDateStr = (d) => {
|
|
96
|
+
if (!d) return "";
|
|
97
|
+
if (d.value) return d.value;
|
|
98
|
+
return d instanceof Date ? d.toISOString().slice(0, 10) : String(d);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const toArray = (input) => {
|
|
102
|
+
if (!input) return [];
|
|
103
|
+
if (Array.isArray(input)) return input;
|
|
104
|
+
return Object.values(input);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// FIX: Robustly retrieve rows for THIS entity from any table structure
|
|
108
|
+
const getEntityRows = (dataset) => {
|
|
109
|
+
if (!dataset) return [];
|
|
110
|
+
|
|
111
|
+
// Case A: Map { "312723": [...] } (Standard DataFetcher behavior with entityField)
|
|
112
|
+
if (dataset[entityId]) {
|
|
113
|
+
const val = dataset[entityId];
|
|
114
|
+
return Array.isArray(val) ? val : [val];
|
|
194
115
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
// ==========================================================================================
|
|
220
|
-
this._processExposureOverTime(result, previousResult, date);
|
|
116
|
+
|
|
117
|
+
// Case B: Flat Array [ row, row ] (Fallback for tables without entityField config)
|
|
118
|
+
if (Array.isArray(dataset)) {
|
|
119
|
+
// Check if the array actually contains rows or nested arrays (from bad Object.values)
|
|
120
|
+
if (dataset.length > 0 && Array.isArray(dataset[0])) {
|
|
121
|
+
// Flatten if we somehow got [[rows], [rows]]
|
|
122
|
+
return dataset.flat().filter(r => String(r.pi_id || r.user_id || r.cid) === String(entityId));
|
|
123
|
+
}
|
|
124
|
+
return dataset.filter(r => String(r.pi_id || r.user_id || r.cid) === String(entityId));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return [];
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const sortAsc = (input) => {
|
|
131
|
+
const arr = toArray(input);
|
|
132
|
+
return arr.sort((a, b) => {
|
|
133
|
+
const dA = toDateStr(a.date);
|
|
134
|
+
const dB = toDateStr(b.date);
|
|
135
|
+
if (!dA) return 1;
|
|
136
|
+
if (!dB) return -1;
|
|
137
|
+
return dA.localeCompare(dB);
|
|
138
|
+
});
|
|
139
|
+
};
|
|
221
140
|
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
141
|
+
// Mappings (Global, not per-entity)
|
|
142
|
+
const tickerMap = new Map();
|
|
143
|
+
toArray(data['ticker_mappings']).forEach(row => {
|
|
144
|
+
if (row.instrument_id && row.ticker) tickerMap.set(Number(row.instrument_id), row.ticker.toUpperCase());
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const sectorMap = new Map();
|
|
148
|
+
toArray(data['sector_mappings']).forEach(row => {
|
|
149
|
+
if (row.symbol && row.sector) sectorMap.set(row.symbol.toUpperCase(), row.sector);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const resolveTicker = (id) => tickerMap.get(Number(id)) || `ID:${id}`;
|
|
153
|
+
const resolveSector = (id) => {
|
|
154
|
+
const ticker = tickerMap.get(Number(id));
|
|
155
|
+
return ticker ? (sectorMap.get(ticker) || 'Unknown') : 'Unknown';
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Initialize Result
|
|
159
|
+
const result = {
|
|
232
160
|
username: null,
|
|
233
161
|
socialEngagement: { chartType: 'line', data: [] },
|
|
234
162
|
profitablePositions: { chartType: 'bar', data: [] },
|
|
@@ -238,483 +166,268 @@ class PopularInvestorProfileMetrics extends Computation {
|
|
|
238
166
|
assetExposure: { chartType: 'pie', data: {} },
|
|
239
167
|
portfolioSummary: { totalInvested: 0, totalProfit: 0, profitPercent: 0 },
|
|
240
168
|
rankingsData: { aum: 0, riskScore: 0, gain: 0, copiers: 0, winRatio: 0, trades: 0 },
|
|
241
|
-
ratingsData: {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
ratingsOverTime: { chartType: 'line', data: [] }
|
|
245
|
-
},
|
|
246
|
-
pageViewsData: {
|
|
247
|
-
totalViews: 0,
|
|
248
|
-
uniqueViewers: 0,
|
|
249
|
-
viewsOverTime: { chartType: 'bar', data: [] }
|
|
250
|
-
},
|
|
251
|
-
watchlistData: {
|
|
252
|
-
totalUsers: 0,
|
|
253
|
-
publicWatchlistCount: 0,
|
|
254
|
-
privateWatchlistCount: 0,
|
|
255
|
-
watchlistOverTime: { chartType: 'line', data: [] }
|
|
256
|
-
},
|
|
169
|
+
ratingsData: { averageRating: 0, totalRatings: 0, ratingsOverTime: { chartType: 'line', data: [] } },
|
|
170
|
+
pageViewsData: { totalViews: 0, uniqueViewers: 0, viewsOverTime: { chartType: 'bar', data: [] } },
|
|
171
|
+
watchlistData: { totalUsers: 0, watchlistOverTime: { chartType: 'line', data: [] } },
|
|
257
172
|
alertHistoryData: { triggeredAlerts: [], alertCountsOverTime: { chartType: 'bar', data: [] } },
|
|
258
173
|
sectorExposureOverTime: { chartType: 'line', data: [] },
|
|
259
174
|
assetExposureOverTime: { chartType: 'line', data: [] },
|
|
260
175
|
alertMetrics: { totalLast7Days: 0, breakdown: {} }
|
|
261
176
|
};
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
instrumentToTicker: {},
|
|
267
|
-
instrumentToSector: {}
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
if (!tickerMappings) return mappings;
|
|
177
|
+
|
|
178
|
+
// ==========================================================================================
|
|
179
|
+
// 1. DATA RETRIEVAL (Using Fixed 'getEntityRows')
|
|
180
|
+
// ==========================================================================================
|
|
271
181
|
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
182
|
+
// These tables are keyed by 'user_id' (same as entityId) or 'pi_id' (same value)
|
|
183
|
+
const portfolios = sortAsc(getEntityRows(data['portfolio_snapshots']));
|
|
184
|
+
const historyData = sortAsc(getEntityRows(data['trade_history_snapshots']));
|
|
185
|
+
const socialData = sortAsc(getEntityRows(data['social_post_snapshots']));
|
|
186
|
+
const rankings = sortAsc(getEntityRows(data['pi_rankings']));
|
|
187
|
+
const ratings = sortAsc(getEntityRows(data['pi_ratings']));
|
|
188
|
+
const pageViews = sortAsc(getEntityRows(data['pi_page_views']));
|
|
189
|
+
const watchlists = sortAsc(getEntityRows(data['watchlist_membership']));
|
|
190
|
+
const alerts = sortAsc(getEntityRows(data['pi_alert_history']));
|
|
191
|
+
const masterList = getEntityRows(data['pi_master_list']);
|
|
192
|
+
|
|
193
|
+
const currentPortfolio = portfolios.length > 0 ? portfolios[portfolios.length - 1] : null;
|
|
194
|
+
const currentRanking = rankings.length > 0 ? rankings[rankings.length - 1] : null;
|
|
195
|
+
|
|
196
|
+
// ==========================================================================================
|
|
197
|
+
// 2. USERNAME
|
|
198
|
+
// ==========================================================================================
|
|
199
|
+
if (masterList.length > 0 && masterList[0].username) {
|
|
200
|
+
result.username = masterList[0].username;
|
|
201
|
+
} else if (currentRanking) {
|
|
202
|
+
result.username = currentRanking.username || currentRanking.UserName || null;
|
|
203
|
+
if (!result.username) {
|
|
204
|
+
const rData = rules.rankings.extractRankingsData(currentRanking);
|
|
205
|
+
result.username = rData?.UserName || null;
|
|
289
206
|
}
|
|
290
207
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
let userPosts = [];
|
|
307
|
-
if (Array.isArray(dailyData)) {
|
|
308
|
-
userPosts = dailyData.filter(p => String(p.user_id || p.userId) === String(userId));
|
|
309
|
-
} else if (dailyData[userId]) {
|
|
310
|
-
userPosts = Array.isArray(dailyData[userId]) ? dailyData[userId] : [dailyData[userId]];
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if (userPosts.length > 0) {
|
|
314
|
-
let likes = 0, comments = 0;
|
|
315
|
-
for (const post of userPosts) {
|
|
316
|
-
likes += (post.LikesCount || post.likes || 0);
|
|
317
|
-
comments += (post.CommentsCount || post.comments || 0);
|
|
318
|
-
}
|
|
319
|
-
result.socialEngagement.data.push({ date: dateStr, likes, comments });
|
|
208
|
+
|
|
209
|
+
// ==========================================================================================
|
|
210
|
+
// 3. RANKINGS
|
|
211
|
+
// ==========================================================================================
|
|
212
|
+
if (currentRanking) {
|
|
213
|
+
const rData = rules.rankings.extractRankingsData(currentRanking);
|
|
214
|
+
if (rData) {
|
|
215
|
+
result.rankingsData = {
|
|
216
|
+
aum: rules.rankings.getAUMTier(rData),
|
|
217
|
+
riskScore: rules.rankings.getRiskScore(rData),
|
|
218
|
+
gain: rules.rankings.getTotalGain(rData),
|
|
219
|
+
copiers: rules.rankings.getCopiers(rData),
|
|
220
|
+
winRatio: rules.rankings.getWinRatio(rData),
|
|
221
|
+
trades: rules.rankings.getTotalTrades(rData)
|
|
222
|
+
};
|
|
320
223
|
}
|
|
321
224
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const trades = this._extractTrades(historyDoc);
|
|
225
|
+
|
|
226
|
+
// ==========================================================================================
|
|
227
|
+
// 4. TRADES
|
|
228
|
+
// ==========================================================================================
|
|
328
229
|
const profitableMap = new Map();
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
230
|
+
const allClosedTrades = [];
|
|
231
|
+
|
|
232
|
+
historyData.forEach(dayDoc => {
|
|
233
|
+
const trades = rules.trades.extractTrades(dayDoc);
|
|
234
|
+
trades.forEach(trade => {
|
|
235
|
+
const closeDate = rules.trades.getCloseDate(trade);
|
|
236
|
+
if (!closeDate) return;
|
|
237
|
+
|
|
238
|
+
const dKey = closeDate.toISOString().split('T')[0];
|
|
239
|
+
const profit = rules.trades.getNetProfit(trade);
|
|
240
|
+
|
|
241
|
+
const entry = profitableMap.get(dKey) || { date: dKey, profitableCount: 0, totalCount: 0 };
|
|
242
|
+
entry.totalCount++;
|
|
243
|
+
if (profit > 0) entry.profitableCount++;
|
|
244
|
+
profitableMap.set(dKey, entry);
|
|
245
|
+
|
|
246
|
+
allClosedTrades.push({
|
|
247
|
+
ticker: resolveTicker(rules.trades.getInstrumentId(trade)),
|
|
248
|
+
closeDate: closeDate,
|
|
249
|
+
netProfit: profit,
|
|
250
|
+
direction: rules.trades.isBuy(trade) ? 'Buy' : 'Sell'
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
347
255
|
result.profitablePositions.data = Array.from(profitableMap.values())
|
|
348
256
|
.sort((a, b) => a.date.localeCompare(b.date))
|
|
349
257
|
.slice(-30);
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
_processTopWinningPositions(result, portfolio, historyDoc, mappings) {
|
|
353
|
-
const topWinning = [];
|
|
354
|
-
|
|
355
|
-
// From current portfolio
|
|
356
|
-
if (portfolio) {
|
|
357
|
-
const positions = this._extractPositions(portfolio);
|
|
358
|
-
for (const pos of positions) {
|
|
359
|
-
const instrumentId = pos.InstrumentID || pos.instrumentId || pos.instrument_id;
|
|
360
|
-
const netProfit = pos.NetProfit || pos.netProfit || pos.net_profit || 0;
|
|
361
|
-
const invested = pos.Amount || pos.amount || pos.invested || 0;
|
|
362
|
-
const value = pos.Value || pos.value || invested + netProfit;
|
|
363
|
-
|
|
364
|
-
if (netProfit > 0 && instrumentId) {
|
|
365
|
-
const ticker = mappings.instrumentToTicker[String(instrumentId)] || `ID${instrumentId}`;
|
|
366
|
-
topWinning.push({ instrumentId, ticker, netProfit, invested, value, isCurrent: true, closeDate: null });
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// From trade history
|
|
372
|
-
if (historyDoc) {
|
|
373
|
-
const trades = this._extractTrades(historyDoc);
|
|
374
|
-
for (const trade of trades) {
|
|
375
|
-
const netProfit = trade.NetProfit || trade.netProfit || trade.net_profit || 0;
|
|
376
|
-
const instrumentId = trade.InstrumentID || trade.instrumentId || trade.instrument_id;
|
|
377
|
-
|
|
378
|
-
if (netProfit > 0 && instrumentId) {
|
|
379
|
-
const ticker = mappings.instrumentToTicker[String(instrumentId)] || `ID${instrumentId}`;
|
|
380
|
-
const closeDate = trade.CloseDateTime || trade.closeDateTime || trade.close_date || null;
|
|
381
|
-
topWinning.push({ instrumentId, ticker, netProfit, invested: 0, value: 0, isCurrent: false, closeDate });
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
result.topWinningPositions.data = topWinning
|
|
258
|
+
|
|
259
|
+
result.topWinningPositions.data = allClosedTrades
|
|
387
260
|
.sort((a, b) => b.netProfit - a.netProfit)
|
|
388
|
-
.slice(0,
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
const
|
|
401
|
-
const
|
|
402
|
-
const invested = pos.Amount || pos.amount || pos.invested || 0;
|
|
403
|
-
|
|
404
|
-
if (instrumentId && invested > 0) {
|
|
405
|
-
const sector = mappings.instrumentToSector[String(instrumentId)] || 'Unknown';
|
|
406
|
-
const weightedProfit = invested * (netProfit / 100);
|
|
407
|
-
const existing = sectorProfits.get(sector) || { totalProfit: 0, totalInvested: 0 };
|
|
408
|
-
existing.totalProfit += weightedProfit;
|
|
409
|
-
existing.totalInvested += invested;
|
|
410
|
-
sectorProfits.set(sector, existing);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
let bestSector = null, worstSector = null;
|
|
415
|
-
let bestProfit = -Infinity, worstProfit = Infinity;
|
|
416
|
-
|
|
417
|
-
for (const [sector, data] of sectorProfits.entries()) {
|
|
418
|
-
const profitPercent = data.totalInvested > 0 ? (data.totalProfit / data.totalInvested) * 100 : 0;
|
|
419
|
-
if (profitPercent > bestProfit) { bestProfit = profitPercent; bestSector = sector; }
|
|
420
|
-
if (profitPercent < worstProfit) { worstProfit = profitPercent; worstSector = sector; }
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
result.sectorPerformance = {
|
|
424
|
-
bestSector,
|
|
425
|
-
worstSector,
|
|
426
|
-
bestSectorProfit: bestProfit !== -Infinity ? bestProfit : 0,
|
|
427
|
-
worstSectorProfit: worstProfit !== Infinity ? worstProfit : 0
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
_processExposureAndSummary(result, portfolio, mappings) {
|
|
432
|
-
if (!portfolio) return;
|
|
433
|
-
|
|
434
|
-
const positions = this._extractPositions(portfolio);
|
|
435
|
-
const sectorMap = {};
|
|
436
|
-
const assetMap = {};
|
|
437
|
-
let total = 0, totalInvested = 0, totalProfit = 0;
|
|
438
|
-
|
|
439
|
-
for (const pos of positions) {
|
|
440
|
-
const instrumentId = pos.InstrumentID || pos.instrumentId || pos.instrument_id;
|
|
441
|
-
const invested = pos.Amount || pos.amount || pos.invested || 0;
|
|
442
|
-
const netProfit = pos.NetProfit || pos.netProfit || pos.net_profit || 0;
|
|
443
|
-
const value = pos.Value || pos.value || invested + netProfit;
|
|
444
|
-
|
|
445
|
-
total += value;
|
|
446
|
-
totalInvested += invested;
|
|
447
|
-
totalProfit += invested * (netProfit / 100);
|
|
448
|
-
|
|
449
|
-
const sector = mappings.instrumentToSector[String(instrumentId)] || 'Unknown';
|
|
450
|
-
sectorMap[sector] = (sectorMap[sector] || 0) + value;
|
|
261
|
+
.slice(0, 5)
|
|
262
|
+
.map(t => ({
|
|
263
|
+
ticker: t.ticker,
|
|
264
|
+
profit: Number(t.netProfit.toFixed(2)),
|
|
265
|
+
date: t.closeDate.toISOString().slice(0, 10),
|
|
266
|
+
direction: t.direction
|
|
267
|
+
}));
|
|
268
|
+
|
|
269
|
+
// ==========================================================================================
|
|
270
|
+
// 5. PORTFOLIO & SECTOR EXPOSURE
|
|
271
|
+
// ==========================================================================================
|
|
272
|
+
if (currentPortfolio) {
|
|
273
|
+
const pData = rules.portfolio.extractPortfolioData(currentPortfolio);
|
|
274
|
+
const positions = rules.portfolio.extractPositions(pData);
|
|
451
275
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
276
|
+
let totalInvested = 0, totalProfit = 0;
|
|
277
|
+
const secMap = {};
|
|
278
|
+
const assetMap = {};
|
|
279
|
+
const sectorProfits = {};
|
|
280
|
+
|
|
281
|
+
positions.forEach(pos => {
|
|
282
|
+
const id = rules.portfolio.getInstrumentId(pos);
|
|
283
|
+
const invested = rules.portfolio.getInvested(pos);
|
|
284
|
+
const netProfit = rules.portfolio.getNetProfit(pos);
|
|
285
|
+
|
|
286
|
+
totalInvested += invested;
|
|
287
|
+
totalProfit += (invested * (netProfit / 100));
|
|
288
|
+
|
|
289
|
+
const ticker = resolveTicker(id);
|
|
290
|
+
const sector = resolveSector(id);
|
|
291
|
+
|
|
292
|
+
assetMap[ticker] = (assetMap[ticker] || 0) + invested;
|
|
293
|
+
secMap[sector] = (secMap[sector] || 0) + invested;
|
|
294
|
+
|
|
295
|
+
if (!sectorProfits[sector]) sectorProfits[sector] = { profit: 0, weight: 0 };
|
|
296
|
+
sectorProfits[sector].profit += (invested * (netProfit / 100));
|
|
297
|
+
sectorProfits[sector].weight += invested;
|
|
459
298
|
});
|
|
460
|
-
|
|
299
|
+
|
|
300
|
+
result.portfolioSummary = {
|
|
301
|
+
totalInvested: Number(totalInvested.toFixed(2)),
|
|
302
|
+
totalProfit: Number(totalProfit.toFixed(2)),
|
|
303
|
+
profitPercent: totalInvested > 0 ? Number(((totalProfit / totalInvested) * 100).toFixed(2)) : 0
|
|
304
|
+
};
|
|
305
|
+
|
|
461
306
|
Object.entries(assetMap)
|
|
462
|
-
.sort(([,
|
|
307
|
+
.sort(([,a], [,b]) => b - a)
|
|
463
308
|
.slice(0, 10)
|
|
464
|
-
.forEach(([k, v]) =>
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
result.rankingsData = {
|
|
481
|
-
aum: rankEntry.AUMTierDesc || rankEntry.Aum || rankEntry.aum || 0,
|
|
482
|
-
riskScore: rankEntry.RiskScore || rankEntry.riskScore || 0,
|
|
483
|
-
gain: rankEntry.Gain || rankEntry.gain || 0,
|
|
484
|
-
copiers: rankEntry.Copiers || rankEntry.copiers || 0,
|
|
485
|
-
winRatio: rankEntry.WinRatio || rankEntry.winRatio || 0,
|
|
486
|
-
trades: rankEntry.Trades || rankEntry.trades || 0
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
_findLatestRankEntry(rankings, userId) {
|
|
491
|
-
if (!rankings) return null;
|
|
492
|
-
|
|
493
|
-
// rankings is date-keyed: { "2026-01-24": [...], ... }
|
|
494
|
-
if (typeof rankings === 'object' && !Array.isArray(rankings)) {
|
|
495
|
-
const dates = Object.keys(rankings).sort().reverse();
|
|
496
|
-
for (const dateStr of dates) {
|
|
497
|
-
const dayRankings = rankings[dateStr];
|
|
498
|
-
if (Array.isArray(dayRankings)) {
|
|
499
|
-
const entry = dayRankings.find(r =>
|
|
500
|
-
String(r.CustomerId || r.pi_id || r.userId) === String(userId)
|
|
501
|
-
);
|
|
502
|
-
if (entry) return entry;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Direct array
|
|
508
|
-
if (Array.isArray(rankings)) {
|
|
509
|
-
return rankings.find(r =>
|
|
510
|
-
String(r.CustomerId || r.pi_id || r.userId) === String(userId)
|
|
511
|
-
);
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
return null;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
_processRatingsData(result, ratings, userId) {
|
|
518
|
-
if (!ratings) return;
|
|
519
|
-
|
|
520
|
-
const dates = Object.keys(ratings).sort();
|
|
521
|
-
let sumRatings = 0, countRatings = 0;
|
|
522
|
-
|
|
523
|
-
for (const dateStr of dates) {
|
|
524
|
-
const dayRatings = ratings[dateStr];
|
|
525
|
-
const userRating = this._findUserData(dayRatings, userId);
|
|
526
|
-
|
|
527
|
-
if (userRating) {
|
|
528
|
-
const avg = userRating.average_rating || userRating.avgRating || 0;
|
|
529
|
-
const total = userRating.total_ratings || userRating.totalRatings || 0;
|
|
530
|
-
|
|
531
|
-
result.ratingsData.ratingsOverTime.data.push({
|
|
532
|
-
date: dateStr,
|
|
533
|
-
rating: avg,
|
|
534
|
-
count: total
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
if (avg > 0) {
|
|
538
|
-
sumRatings += avg;
|
|
539
|
-
countRatings++;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
result.ratingsData.totalRatings = total;
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
if (countRatings > 0) {
|
|
547
|
-
result.ratingsData.averageRating = Number((sumRatings / countRatings).toFixed(2));
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
_processPageViewsData(result, pageViews, userId) {
|
|
552
|
-
if (!pageViews) return;
|
|
553
|
-
|
|
554
|
-
const dates = Object.keys(pageViews).sort();
|
|
555
|
-
|
|
556
|
-
for (const dateStr of dates) {
|
|
557
|
-
const dayViews = pageViews[dateStr];
|
|
558
|
-
const userViews = this._findUserData(dayViews, userId);
|
|
559
|
-
|
|
560
|
-
if (userViews) {
|
|
561
|
-
const views = userViews.total_views || userViews.totalViews || 0;
|
|
562
|
-
const uniques = userViews.unique_viewers || userViews.uniqueViewers || 0;
|
|
563
|
-
|
|
564
|
-
result.pageViewsData.viewsOverTime.data.push({
|
|
565
|
-
date: dateStr,
|
|
566
|
-
views: views
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
result.pageViewsData.totalViews += views;
|
|
570
|
-
result.pageViewsData.uniqueViewers = uniques;
|
|
571
|
-
}
|
|
309
|
+
.forEach(([k, v]) => result.assetExposure.data[k] = Number(v.toFixed(2)));
|
|
310
|
+
|
|
311
|
+
Object.entries(secMap)
|
|
312
|
+
.forEach(([k, v]) => result.sectorExposure.data[k] = Number(v.toFixed(2)));
|
|
313
|
+
|
|
314
|
+
let bestS = null, worstS = null, bestP = -Infinity, worstP = Infinity;
|
|
315
|
+
Object.entries(sectorProfits).forEach(([sec, d]) => {
|
|
316
|
+
if (d.weight <= 0) return;
|
|
317
|
+
const p = (d.profit / d.weight) * 100;
|
|
318
|
+
if (p > bestP) { bestP = p; bestS = sec; }
|
|
319
|
+
if (p < worstP) { worstP = p; worstS = sec; }
|
|
320
|
+
});
|
|
321
|
+
result.sectorPerformance = {
|
|
322
|
+
bestSector: bestS, bestSectorProfit: bestS ? Number(bestP.toFixed(2)) : 0,
|
|
323
|
+
worstSector: worstS, worstSectorProfit: worstS ? Number(worstP.toFixed(2)) : 0
|
|
324
|
+
};
|
|
572
325
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
const dayWatchlist = watchlist[dateStr];
|
|
582
|
-
const userWatchlist = this._findUserData(dayWatchlist, userId);
|
|
326
|
+
|
|
327
|
+
// ==========================================================================================
|
|
328
|
+
// 6. EXPOSURE OVER TIME
|
|
329
|
+
// ==========================================================================================
|
|
330
|
+
portfolios.forEach(entry => {
|
|
331
|
+
const pData = rules.portfolio.extractPortfolioData(entry);
|
|
332
|
+
const positions = rules.portfolio.extractPositions(pData);
|
|
333
|
+
const dateStr = toDateStr(entry.date);
|
|
583
334
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
result.watchlistData.watchlistOverTime.data.push({
|
|
588
|
-
date: dateStr,
|
|
589
|
-
count: total
|
|
590
|
-
});
|
|
591
|
-
|
|
592
|
-
result.watchlistData.totalUsers = total;
|
|
593
|
-
result.watchlistData.publicWatchlistCount = userWatchlist.public_watchlist_count || userWatchlist.publicCount || 0;
|
|
594
|
-
result.watchlistData.privateWatchlistCount = userWatchlist.private_watchlist_count || userWatchlist.privateCount || 0;
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
_processAlertHistory(result, alerts, userId) {
|
|
600
|
-
if (!alerts) return;
|
|
601
|
-
|
|
602
|
-
const dates = Object.keys(alerts).sort();
|
|
603
|
-
let totalAlertsLast7Days = 0;
|
|
604
|
-
const alertBreakdown = {};
|
|
605
|
-
|
|
606
|
-
for (const dateStr of dates) {
|
|
607
|
-
const dayAlerts = alerts[dateStr];
|
|
608
|
-
const userAlerts = this._findUserData(dayAlerts, userId);
|
|
335
|
+
const dailySec = {};
|
|
336
|
+
const dailyAsset = {};
|
|
609
337
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
if (
|
|
643
|
-
result.
|
|
338
|
+
positions.forEach(pos => {
|
|
339
|
+
const id = rules.portfolio.getInstrumentId(pos);
|
|
340
|
+
const invested = rules.portfolio.getInvested(pos);
|
|
341
|
+
const ticker = resolveTicker(id);
|
|
342
|
+
const sector = resolveSector(id);
|
|
343
|
+
dailySec[sector] = (dailySec[sector] || 0) + invested;
|
|
344
|
+
dailyAsset[ticker] = (dailyAsset[ticker] || 0) + invested;
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
result.sectorExposureOverTime.data.push({ date: dateStr, sectors: dailySec });
|
|
348
|
+
result.assetExposureOverTime.data.push({ date: dateStr, assets: dailyAsset });
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// ==========================================================================================
|
|
352
|
+
// 7. SOCIAL, RATINGS, PAGEVIEWS, WATCHLIST
|
|
353
|
+
// ==========================================================================================
|
|
354
|
+
const sevenDaysAgo = new Date(new Date(date).setDate(new Date(date).getDate() - 7)).toISOString().slice(0,10);
|
|
355
|
+
|
|
356
|
+
socialData.filter(d => toDateStr(d.date) >= sevenDaysAgo).forEach(day => {
|
|
357
|
+
const posts = rules.social.extractPosts(day);
|
|
358
|
+
let likes = 0, comments = 0;
|
|
359
|
+
posts.forEach(p => {
|
|
360
|
+
likes += (rules.social.getPostLikes(p) || 0);
|
|
361
|
+
comments += (rules.social.getPostComments(p) || 0);
|
|
362
|
+
});
|
|
363
|
+
result.socialEngagement.data.push({ date: toDateStr(day.date), likes, comments });
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
ratings.forEach(r => {
|
|
367
|
+
const val = Number(r.average_rating || 0);
|
|
368
|
+
const count = Number(r.total_ratings || 0);
|
|
369
|
+
const d = toDateStr(r.date);
|
|
370
|
+
if (d) {
|
|
371
|
+
result.ratingsData.ratingsOverTime.data.push({ date: d, rating: val, count });
|
|
372
|
+
result.ratingsData.totalRatings = count;
|
|
373
|
+
result.ratingsData.averageRating = val;
|
|
644
374
|
}
|
|
645
|
-
|
|
646
|
-
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
pageViews.forEach(p => {
|
|
378
|
+
const views = Number(p.total_views || 0);
|
|
379
|
+
const d = toDateStr(p.date);
|
|
380
|
+
if (d) {
|
|
381
|
+
result.pageViewsData.viewsOverTime.data.push({ date: d, views });
|
|
382
|
+
result.pageViewsData.totalViews += views;
|
|
383
|
+
result.pageViewsData.uniqueViewers = Number(p.unique_viewers || 0);
|
|
647
384
|
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
result.
|
|
656
|
-
} else {
|
|
657
|
-
result.sectorExposureOverTime.data.push(todaySectorEntry);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
watchlists.forEach(w => {
|
|
388
|
+
const count = Number(w.total_users || 0);
|
|
389
|
+
const d = toDateStr(w.date);
|
|
390
|
+
if (d) {
|
|
391
|
+
result.watchlistData.watchlistOverTime.data.push({ date: d, count });
|
|
392
|
+
result.watchlistData.totalUsers = count;
|
|
658
393
|
}
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// ==========================================================================================
|
|
397
|
+
// 8. ALERTS HISTORY
|
|
398
|
+
// ==========================================================================================
|
|
399
|
+
const alertsWindow = alerts.filter(d => toDateStr(d.date) >= sevenDaysAgo);
|
|
400
|
+
const alertsByDate = {};
|
|
401
|
+
|
|
402
|
+
alertsWindow.forEach(row => {
|
|
403
|
+
const d = toDateStr(row.date);
|
|
404
|
+
const type = row.alert_type;
|
|
405
|
+
const count = Number(row.trigger_count || 1);
|
|
406
|
+
|
|
407
|
+
if (type && type.toLowerCase().includes('test')) return;
|
|
408
|
+
|
|
409
|
+
result.alertMetrics.totalLast7Days += count;
|
|
410
|
+
result.alertMetrics.breakdown[type] = (result.alertMetrics.breakdown[type] || 0) + count;
|
|
411
|
+
alertsByDate[d] = (alertsByDate[d] || 0) + count;
|
|
412
|
+
|
|
413
|
+
if (d === date) {
|
|
414
|
+
result.alertHistoryData.triggeredAlerts.push({
|
|
415
|
+
alertType: type,
|
|
416
|
+
triggered: true,
|
|
417
|
+
count: count,
|
|
418
|
+
metadata: row.metadata || {}
|
|
419
|
+
});
|
|
668
420
|
}
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
// =========================================================================
|
|
681
|
-
// DATA EXTRACTION HELPERS
|
|
682
|
-
// =========================================================================
|
|
683
|
-
|
|
684
|
-
_extractPositions(portfolio) {
|
|
685
|
-
if (!portfolio) return [];
|
|
686
|
-
if (Array.isArray(portfolio)) return portfolio;
|
|
687
|
-
if (portfolio.AggregatedPositions) return portfolio.AggregatedPositions;
|
|
688
|
-
if (portfolio.Positions) return portfolio.Positions;
|
|
689
|
-
if (portfolio.positions) return portfolio.positions;
|
|
690
|
-
return [];
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
_extractTrades(historyDoc) {
|
|
694
|
-
if (!historyDoc) return [];
|
|
695
|
-
if (Array.isArray(historyDoc)) return historyDoc;
|
|
696
|
-
if (historyDoc.Trades) return historyDoc.Trades;
|
|
697
|
-
if (historyDoc.trades) return historyDoc.trades;
|
|
698
|
-
if (historyDoc.History) return historyDoc.History;
|
|
699
|
-
return [];
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
_findUserData(dayData, userId) {
|
|
703
|
-
if (!dayData) return null;
|
|
704
|
-
|
|
705
|
-
// Direct lookup by user ID
|
|
706
|
-
if (dayData[userId]) return dayData[userId];
|
|
707
|
-
if (dayData[String(userId)]) return dayData[String(userId)];
|
|
708
|
-
|
|
709
|
-
// Array - find by user ID field
|
|
710
|
-
if (Array.isArray(dayData)) {
|
|
711
|
-
return dayData.find(item =>
|
|
712
|
-
String(item.pi_id || item.user_id || item.userId) === String(userId)
|
|
713
|
-
);
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
return null;
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
Object.entries(alertsByDate)
|
|
424
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
425
|
+
.forEach(([d, count]) => {
|
|
426
|
+
result.alertHistoryData.alertCountsOverTime.data.push({ date: d, count });
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
this.setResult(entityId, result);
|
|
717
430
|
}
|
|
718
431
|
}
|
|
719
432
|
|
|
720
|
-
module.exports = PopularInvestorProfileMetrics;
|
|
433
|
+
module.exports = PopularInvestorProfileMetrics;
|