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
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const { Computation } = require('../framework');
|
|
2
|
+
|
|
3
|
+
class DebugSignedInUsers extends Computation {
|
|
4
|
+
static getConfig() {
|
|
5
|
+
return {
|
|
6
|
+
name: 'DebugSignedInUsers',
|
|
7
|
+
type: 'per-entity',
|
|
8
|
+
category: 'debug',
|
|
9
|
+
|
|
10
|
+
// CRITICAL: Tells the Orchestrator which partition to use
|
|
11
|
+
userType: 'SIGNED_IN_USER',
|
|
12
|
+
|
|
13
|
+
requires: {
|
|
14
|
+
// DRIVER TABLE: This MUST be first.
|
|
15
|
+
// The DAG will only create tasks for Entity IDs found in this filtered query.
|
|
16
|
+
'portfolio_snapshots': {
|
|
17
|
+
lookback: 0, // Just need today's existence
|
|
18
|
+
mandatory: true,
|
|
19
|
+
fields: ['user_id', 'date'],
|
|
20
|
+
filter: { user_type: 'SIGNED_IN_USER' }
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
storage: {
|
|
25
|
+
// Just log to console or a debug table for now
|
|
26
|
+
bigquery: false,
|
|
27
|
+
firestore: { enabled: false }
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async process(context) {
|
|
33
|
+
const { entityId, data } = context;
|
|
34
|
+
|
|
35
|
+
// If the Orchestrator works correctly, we should ONLY see Signed-In Users here.
|
|
36
|
+
// We verify by returning their ID.
|
|
37
|
+
|
|
38
|
+
const row = data['portfolio_snapshots'];
|
|
39
|
+
|
|
40
|
+
// Sanity Check: If row exists, we are good.
|
|
41
|
+
if (row) {
|
|
42
|
+
this.setResult(entityId, {
|
|
43
|
+
status: 'Found',
|
|
44
|
+
userType: 'SIGNED_IN_USER',
|
|
45
|
+
checkedAt: new Date().toISOString()
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = DebugSignedInUsers;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const { Computation } = require('../framework');
|
|
2
|
+
|
|
3
|
+
class SignedInUserMirrorHistory extends Computation {
|
|
4
|
+
static getConfig() {
|
|
5
|
+
return {
|
|
6
|
+
name: 'SignedInUserMirrorHistory',
|
|
7
|
+
type: 'per-entity',
|
|
8
|
+
category: 'signed_in_user',
|
|
9
|
+
|
|
10
|
+
// CRITICAL: Must be historical to accumulate the "Past" list over time
|
|
11
|
+
// without needing huge lookbacks.
|
|
12
|
+
isHistorical: true,
|
|
13
|
+
|
|
14
|
+
requires: {
|
|
15
|
+
'portfolio_snapshots': {
|
|
16
|
+
lookback: 0,
|
|
17
|
+
mandatory: true,
|
|
18
|
+
fields: ['user_id', 'portfolio_data', 'date']
|
|
19
|
+
},
|
|
20
|
+
// Reduced from 90 to 30 to comply with DAG limits
|
|
21
|
+
'trade_history_snapshots': {
|
|
22
|
+
lookback: 30,
|
|
23
|
+
mandatory: false,
|
|
24
|
+
fields: ['user_id', 'history_data']
|
|
25
|
+
},
|
|
26
|
+
'pi_master_list': {
|
|
27
|
+
lookback: 1,
|
|
28
|
+
mandatory: false,
|
|
29
|
+
fields: ['cid', 'username']
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
storage: {
|
|
34
|
+
bigquery: true,
|
|
35
|
+
firestore: {
|
|
36
|
+
enabled: true,
|
|
37
|
+
path: 'users/{entityId}/mirror_history',
|
|
38
|
+
merge: true
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async process(context) {
|
|
45
|
+
const { data, entityId, rules, previousResult } = context;
|
|
46
|
+
|
|
47
|
+
// 1. Setup & Username Map
|
|
48
|
+
const masterList = data['pi_master_list'] || [];
|
|
49
|
+
const usernameMap = new Map();
|
|
50
|
+
const toArray = (input) => Array.isArray(input) ? input : Object.values(input || {});
|
|
51
|
+
|
|
52
|
+
toArray(masterList).forEach(row => {
|
|
53
|
+
if (row.cid && row.username) usernameMap.set(String(row.cid), row.username);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// 2. Determine CURRENT Mirrors (From Today's Portfolio)
|
|
57
|
+
const currentMap = new Map();
|
|
58
|
+
const portfolioRow = data['portfolio_snapshots'];
|
|
59
|
+
|
|
60
|
+
if (portfolioRow) {
|
|
61
|
+
const pData = rules.portfolio.extractPortfolioData(portfolioRow);
|
|
62
|
+
const rawMirrors = rules.portfolio.extractMirrors(pData);
|
|
63
|
+
|
|
64
|
+
rawMirrors.forEach(m => {
|
|
65
|
+
const cid = String(m.ParentCID || m.MirrorID);
|
|
66
|
+
if (cid && cid !== '0') {
|
|
67
|
+
currentMap.set(cid, {
|
|
68
|
+
cid: Number(cid),
|
|
69
|
+
username: usernameMap.get(cid) || m.ParentUsername || 'Unknown',
|
|
70
|
+
invested: m.Invested || 0,
|
|
71
|
+
profit: m.NetProfit || 0,
|
|
72
|
+
status: 'active',
|
|
73
|
+
startedAt: m.InitDate || null
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 3. Determine PAST Mirrors (Merge Previous State + New Closures)
|
|
80
|
+
const pastMap = new Map();
|
|
81
|
+
|
|
82
|
+
// A. Load existing past mirrors from yesterday's result
|
|
83
|
+
if (previousResult && Array.isArray(previousResult.past)) {
|
|
84
|
+
previousResult.past.forEach(pm => pastMap.set(String(pm.cid), pm));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// B. Detect New Closures (Was in Previous Current, NOT in Today's Current)
|
|
88
|
+
if (previousResult && Array.isArray(previousResult.current)) {
|
|
89
|
+
previousResult.current.forEach(prevCurr => {
|
|
90
|
+
const cidStr = String(prevCurr.cid);
|
|
91
|
+
if (!currentMap.has(cidStr)) {
|
|
92
|
+
// This mirror was active yesterday but is gone today -> It closed.
|
|
93
|
+
pastMap.set(cidStr, {
|
|
94
|
+
...prevCurr,
|
|
95
|
+
status: 'closed',
|
|
96
|
+
closedAt: context.date, // Mark closure date as today
|
|
97
|
+
invested: 0 // Reset invested for closed items
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// C. Supplement with Trade History (Only look back 30 days for metadata)
|
|
104
|
+
// This catches any explicit "Stop Copy" events that might provide better metadata
|
|
105
|
+
const historyRows = data['trade_history_snapshots'] || [];
|
|
106
|
+
historyRows.forEach(row => {
|
|
107
|
+
const trades = rules.trades.extractTrades(row);
|
|
108
|
+
trades.forEach(trade => {
|
|
109
|
+
const parentCID = trade.ParentCID || trade.MirrorID || trade.CopyTraderID;
|
|
110
|
+
const cidStr = String(parentCID);
|
|
111
|
+
|
|
112
|
+
// If we found a past interaction that isn't currently active
|
|
113
|
+
if (parentCID && cidStr !== '0' && !currentMap.has(cidStr)) {
|
|
114
|
+
if (!pastMap.has(cidStr)) {
|
|
115
|
+
// Found a copy in history that wasn't in our previous state
|
|
116
|
+
// (e.g. from before we started tracking, or within the 30d window)
|
|
117
|
+
pastMap.set(cidStr, {
|
|
118
|
+
cid: Number(parentCID),
|
|
119
|
+
username: usernameMap.get(cidStr) || 'Unknown',
|
|
120
|
+
status: 'closed',
|
|
121
|
+
lastInteraction: rules.trades.getCloseDate(trade) || row.date
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// 4. Final Output
|
|
129
|
+
this.setResult(entityId, {
|
|
130
|
+
current: Array.from(currentMap.values()),
|
|
131
|
+
past: Array.from(pastMap.values()),
|
|
132
|
+
totalUniqueCopied: currentMap.size + pastMap.size,
|
|
133
|
+
updatedAt: new Date().toISOString()
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = SignedInUserMirrorHistory;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const { Computation } = require('../framework');
|
|
2
|
+
|
|
3
|
+
class SignedInUserPIProfileMetrics extends Computation {
|
|
4
|
+
static getConfig() {
|
|
5
|
+
return {
|
|
6
|
+
name: 'SignedInUserPIProfileMetrics',
|
|
7
|
+
type: 'per-entity',
|
|
8
|
+
category: 'signed_in_user',
|
|
9
|
+
isHistorical: true,
|
|
10
|
+
userType: 'SIGNED_IN_USER',
|
|
11
|
+
|
|
12
|
+
requires: {
|
|
13
|
+
// DRIVER TABLE
|
|
14
|
+
// lookback: 0 prevents "All history" fetch.
|
|
15
|
+
// filter: user_type ensures only signed-in users are fetched.
|
|
16
|
+
'portfolio_snapshots': {
|
|
17
|
+
lookback: 0,
|
|
18
|
+
mandatory: true,
|
|
19
|
+
fields: ['user_id', 'user_type', 'portfolio_data', 'date'],
|
|
20
|
+
filter: { user_type: 'SIGNED_IN_USER' }
|
|
21
|
+
},
|
|
22
|
+
'trade_history_snapshots': {
|
|
23
|
+
lookback: 0,
|
|
24
|
+
fields: ['user_id', 'user_type', 'history_data', 'date'],
|
|
25
|
+
filter: { user_type: 'SIGNED_IN_USER' }
|
|
26
|
+
},
|
|
27
|
+
'ticker_mappings': { mandatory: false, fields: ['instrument_id', 'ticker'] },
|
|
28
|
+
'sector_mappings': { mandatory: false, fields: ['symbol', 'sector'] }
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
storage: {
|
|
32
|
+
bigquery: true,
|
|
33
|
+
firestore: {
|
|
34
|
+
enabled: true,
|
|
35
|
+
path: 'users/{entityId}/pi_profile_metrics/{date}',
|
|
36
|
+
merge: true
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async process(context) {
|
|
43
|
+
const { data, entityId, rules, dataFetcher } = context;
|
|
44
|
+
const portfolioRow = data['portfolio_snapshots'];
|
|
45
|
+
|
|
46
|
+
// Guard clause REMOVED to prove framework filtering works
|
|
47
|
+
|
|
48
|
+
// 2. PI STATUS CHECK (Intersection)
|
|
49
|
+
// Dynamically verify if this specific customer ID exists in the master list.
|
|
50
|
+
const piCheck = await dataFetcher.fetch({
|
|
51
|
+
table: 'pi_master_list',
|
|
52
|
+
filter: { cid: parseInt(entityId, 10) },
|
|
53
|
+
fields: ['cid'],
|
|
54
|
+
lookback: 0
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!piCheck || piCheck.length === 0) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 3. CORE LOGIC
|
|
62
|
+
const result = {
|
|
63
|
+
isPopularInvestor: true,
|
|
64
|
+
portfolioSummary: { totalValue: 0, profitPercent: 0, winRatio: 0 },
|
|
65
|
+
sectorExposure: {},
|
|
66
|
+
updatedAt: new Date().toISOString()
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const pData = rules.portfolio.extractPortfolioData(portfolioRow);
|
|
70
|
+
const positions = rules.portfolio.extractPositions(pData);
|
|
71
|
+
|
|
72
|
+
result.portfolioSummary = {
|
|
73
|
+
totalValue: rules.portfolio.calculateTotalValue(positions),
|
|
74
|
+
profitPercent: rules.portfolio.calculateWeightedProfitPercent(positions),
|
|
75
|
+
winRatio: rules.portfolio.calculateWinRatio(positions)
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const tickerMap = this._buildTickerMap(data['ticker_mappings']);
|
|
79
|
+
const sectorMap = this._buildSectorMap(data['sector_mappings']);
|
|
80
|
+
const instrumentSectorMap = {};
|
|
81
|
+
Object.keys(tickerMap).forEach(instId => {
|
|
82
|
+
const ticker = tickerMap[instId];
|
|
83
|
+
if (sectorMap[ticker]) instrumentSectorMap[instId] = sectorMap[ticker];
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
result.sectorExposure = rules.portfolio.calculateSectorExposure(positions, instrumentSectorMap);
|
|
87
|
+
|
|
88
|
+
this.setResult(entityId, result);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
_buildTickerMap(rows) {
|
|
92
|
+
const map = {};
|
|
93
|
+
const arr = Array.isArray(rows) ? rows : Object.values(rows || {});
|
|
94
|
+
arr.forEach(r => { if (r.instrument_id) map[r.instrument_id] = r.ticker; });
|
|
95
|
+
return map;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
_buildSectorMap(rows) {
|
|
99
|
+
const map = {};
|
|
100
|
+
const arr = Array.isArray(rows) ? rows : Object.values(rows || {});
|
|
101
|
+
arr.forEach(r => { if (r.symbol) map[r.symbol] = r.sector; });
|
|
102
|
+
return map;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = SignedInUserPIProfileMetrics;
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Signed-In User Profile Metrics (v2 Refactor)
|
|
3
|
+
* * TARGETS: 'SIGNED_IN_USER' only (via BQ filters).
|
|
4
|
+
* * STRATEGY: Iterates Portfolio Mirrors to enrich copied PI data.
|
|
5
|
+
*/
|
|
6
|
+
const { Computation } = require('../framework');
|
|
7
|
+
|
|
8
|
+
class SignedInUserProfileMetrics extends Computation {
|
|
9
|
+
|
|
10
|
+
static getConfig() {
|
|
11
|
+
return {
|
|
12
|
+
name: 'SignedInUserProfileMetrics',
|
|
13
|
+
type: 'per-entity',
|
|
14
|
+
category: 'signed_in_user',
|
|
15
|
+
isHistorical: true,
|
|
16
|
+
|
|
17
|
+
requires: {
|
|
18
|
+
// --- Core Data (Drivers) ---
|
|
19
|
+
// [CRITICAL] Filters restrict execution to Signed-In Users only
|
|
20
|
+
'portfolio_snapshots': {
|
|
21
|
+
lookback: 30,
|
|
22
|
+
mandatory: true,
|
|
23
|
+
fields: ['user_id', 'portfolio_data', 'date'],
|
|
24
|
+
filter: { user_type: 'SIGNED_IN_USER' }
|
|
25
|
+
},
|
|
26
|
+
'trade_history_snapshots': {
|
|
27
|
+
lookback: 30,
|
|
28
|
+
mandatory: false,
|
|
29
|
+
fields: ['user_id', 'history_data', 'date'],
|
|
30
|
+
filter: { user_type: 'SIGNED_IN_USER' }
|
|
31
|
+
},
|
|
32
|
+
'social_post_snapshots': {
|
|
33
|
+
lookback: 7,
|
|
34
|
+
mandatory: false,
|
|
35
|
+
fields: ['user_id', 'posts_data', 'date'],
|
|
36
|
+
filter: { user_type: 'SIGNED_IN_USER' }
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
// --- Reference Data (Lookups) ---
|
|
40
|
+
// Used only to enrich PIs found in the user's copy list
|
|
41
|
+
'pi_rankings': {
|
|
42
|
+
lookback: 7,
|
|
43
|
+
mandatory: false,
|
|
44
|
+
fields: ['pi_id', 'rankings_data', 'username', 'date']
|
|
45
|
+
},
|
|
46
|
+
'pi_master_list': {
|
|
47
|
+
lookback: 1,
|
|
48
|
+
mandatory: false,
|
|
49
|
+
fields: ['cid', 'username']
|
|
50
|
+
},
|
|
51
|
+
'pi_ratings': {
|
|
52
|
+
lookback: 30,
|
|
53
|
+
mandatory: false,
|
|
54
|
+
fields: ['pi_id', 'average_rating', 'date']
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// --- Mappings ---
|
|
58
|
+
'ticker_mappings': { mandatory: false },
|
|
59
|
+
'sector_mappings': { mandatory: false }
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
storage: {
|
|
63
|
+
bigquery: true,
|
|
64
|
+
firestore: {
|
|
65
|
+
enabled: true,
|
|
66
|
+
path: 'user_profiles/{entityId}/metrics/{date}', // Standardized path
|
|
67
|
+
merge: true
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
userType: 'SIGNED_IN_USER'
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async process(context) {
|
|
76
|
+
const { data, entityId, rules } = context;
|
|
77
|
+
|
|
78
|
+
// ==========================================================================================
|
|
79
|
+
// 0. HELPERS & REFERENCE DATA
|
|
80
|
+
// ==========================================================================================
|
|
81
|
+
const toDateStr = (d) => (d?.value || (d instanceof Date ? d.toISOString().slice(0, 10) : String(d)));
|
|
82
|
+
const toArray = (input) => (Array.isArray(input) ? input : (input ? Object.values(input) : []));
|
|
83
|
+
|
|
84
|
+
// Helper to handle V2 Map-based data structure
|
|
85
|
+
const getEntityRows = (dataset) => {
|
|
86
|
+
if (!dataset) return [];
|
|
87
|
+
// Direct entity lookup (fastest)
|
|
88
|
+
if (dataset[entityId]) return Array.isArray(dataset[entityId]) ? dataset[entityId] : [dataset[entityId]];
|
|
89
|
+
// Fallback for flat arrays (slower, but safe)
|
|
90
|
+
if (Array.isArray(dataset)) return dataset.filter(r => String(r.user_id || r.cid) === String(entityId));
|
|
91
|
+
return [];
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const sortAsc = (input) => toArray(input).sort((a, b) => toDateStr(a.date).localeCompare(toDateStr(b.date)));
|
|
95
|
+
|
|
96
|
+
// Mappings
|
|
97
|
+
const tickerMap = new Map();
|
|
98
|
+
toArray(data['ticker_mappings']).forEach(row => { if (row.instrument_id) tickerMap.set(Number(row.instrument_id), row.ticker); });
|
|
99
|
+
const resolveTicker = (id) => tickerMap.get(Number(id)) || `ID:${id}`;
|
|
100
|
+
|
|
101
|
+
const sectorMap = new Map();
|
|
102
|
+
toArray(data['sector_mappings']).forEach(row => { if (row.symbol) sectorMap.set(row.symbol.toUpperCase(), row.sector); });
|
|
103
|
+
const resolveSector = (id) => {
|
|
104
|
+
const ticker = resolveTicker(id);
|
|
105
|
+
return ticker ? (sectorMap.get(ticker) || 'Unknown') : 'Unknown';
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Global Reference Data (Optimized for Lookups)
|
|
109
|
+
// We convert these arrays to Maps/Finders only once if this were a global calc,
|
|
110
|
+
// but here we just do simple lookups for the specific copied PIs.
|
|
111
|
+
const globalRankings = toArray(data['pi_rankings']);
|
|
112
|
+
const piMasterList = toArray(data['pi_master_list']);
|
|
113
|
+
|
|
114
|
+
const resolveUsername = (cid) => {
|
|
115
|
+
const rank = globalRankings.find(r => String(r.pi_id || r.CustomerId) === String(cid));
|
|
116
|
+
if (rank?.username || rank?.UserName) return rank.username || rank.UserName;
|
|
117
|
+
const master = piMasterList.find(m => String(m.cid) === String(cid));
|
|
118
|
+
return master?.username || 'Unknown';
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Initialize Result Structure
|
|
122
|
+
const result = {
|
|
123
|
+
socialEngagement: { chartType: 'line', data: [] },
|
|
124
|
+
myPosts: { chartType: 'feed', data: [] },
|
|
125
|
+
profitablePositions: { chartType: 'bar', data: [] },
|
|
126
|
+
topWinningPositions: { chartType: 'table', data: [] },
|
|
127
|
+
sectorPerformance: { bestSector: null, worstSector: null, bestSectorProfit: 0, worstSectorProfit: 0 },
|
|
128
|
+
sectorExposure: { chartType: 'pie', data: {} },
|
|
129
|
+
assetExposure: { chartType: 'pie', data: {} },
|
|
130
|
+
portfolioSummary: { totalInvested: 0, totalProfit: 0, profitPercent: 0 },
|
|
131
|
+
copiedPIs: { chartType: 'cards', data: [] }, // Populated from Mirrors
|
|
132
|
+
recommendedPIs: { chartType: 'cards', data: [] }, // (Optional: derived from similarity)
|
|
133
|
+
recentlyViewedPages: { chartType: 'list', data: [] },
|
|
134
|
+
alertGraphs: { chartType: 'line', data: [] },
|
|
135
|
+
performanceGraphics: { chartType: 'composite', data: {} },
|
|
136
|
+
exposureGraphics: { chartType: 'composite', data: {} }
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// ==========================================================================================
|
|
140
|
+
// 1. DATA PREPARATION
|
|
141
|
+
// ==========================================================================================
|
|
142
|
+
const portfolios = sortAsc(getEntityRows(data['portfolio_snapshots']));
|
|
143
|
+
const historyData = sortAsc(getEntityRows(data['trade_history_snapshots']));
|
|
144
|
+
const socialData = sortAsc(getEntityRows(data['social_post_snapshots']));
|
|
145
|
+
|
|
146
|
+
const currentPortfolio = portfolios.length > 0 ? portfolios[portfolios.length - 1] : null;
|
|
147
|
+
|
|
148
|
+
// ==========================================================================================
|
|
149
|
+
// 2. SOCIAL FEED (My Posts)
|
|
150
|
+
// ==========================================================================================
|
|
151
|
+
const socialFeed = [];
|
|
152
|
+
socialData.forEach(day => {
|
|
153
|
+
const posts = rules.social.extractPosts(day);
|
|
154
|
+
const dStr = toDateStr(day.date);
|
|
155
|
+
let dayLikes = 0, dayComments = 0;
|
|
156
|
+
|
|
157
|
+
posts.forEach(p => {
|
|
158
|
+
const likes = rules.social.getPostLikes(p) || 0;
|
|
159
|
+
const comments = rules.social.getPostComments(p) || 0;
|
|
160
|
+
dayLikes += likes;
|
|
161
|
+
dayComments += comments;
|
|
162
|
+
|
|
163
|
+
socialFeed.push({
|
|
164
|
+
id: p.id,
|
|
165
|
+
date: rules.social.getPostDate(p),
|
|
166
|
+
text: rules.social.getPostText(p),
|
|
167
|
+
likes,
|
|
168
|
+
comments,
|
|
169
|
+
type: 'post'
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (dayLikes > 0 || dayComments > 0) {
|
|
174
|
+
result.socialEngagement.data.push({ date: dStr, likes: dayLikes, comments: dayComments });
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
// Limit feed size
|
|
178
|
+
result.myPosts.data = socialFeed.sort((a, b) => new Date(b.date) - new Date(a.date)).slice(0, 20);
|
|
179
|
+
|
|
180
|
+
// ==========================================================================================
|
|
181
|
+
// 3. TRADES & PERFORMANCE
|
|
182
|
+
// ==========================================================================================
|
|
183
|
+
const dailyPnL = new Map();
|
|
184
|
+
const winLoss = { wins: 0, losses: 0, profit: 0, loss: 0 };
|
|
185
|
+
const tradeStats = new Map();
|
|
186
|
+
|
|
187
|
+
historyData.forEach(dayDoc => {
|
|
188
|
+
const trades = rules.trades.extractTrades(dayDoc);
|
|
189
|
+
trades.forEach(t => {
|
|
190
|
+
const closeDate = rules.trades.getCloseDate(t);
|
|
191
|
+
if (!closeDate) return;
|
|
192
|
+
|
|
193
|
+
const dKey = closeDate.toISOString().split('T')[0];
|
|
194
|
+
const profit = rules.trades.getNetProfit(t);
|
|
195
|
+
|
|
196
|
+
// Stats for Bar Chart
|
|
197
|
+
const entry = tradeStats.get(dKey) || { date: dKey, profitableCount: 0, totalCount: 0 };
|
|
198
|
+
entry.totalCount++;
|
|
199
|
+
if (profit > 0) entry.profitableCount++;
|
|
200
|
+
tradeStats.set(dKey, entry);
|
|
201
|
+
|
|
202
|
+
// Stats for Aggregate Graphics
|
|
203
|
+
if (profit > 0) { winLoss.wins++; winLoss.profit += profit; }
|
|
204
|
+
else if (profit < 0) { winLoss.losses++; winLoss.loss += profit; }
|
|
205
|
+
|
|
206
|
+
dailyPnL.set(dKey, (dailyPnL.get(dKey) || 0) + profit);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
result.profitablePositions.data = Array.from(tradeStats.values())
|
|
211
|
+
.sort((a,b) => a.date.localeCompare(b.date))
|
|
212
|
+
.slice(-30);
|
|
213
|
+
|
|
214
|
+
result.performanceGraphics.data = {
|
|
215
|
+
winRate: (winLoss.wins + winLoss.losses) > 0 ? Number(((winLoss.wins / (winLoss.wins + winLoss.losses)) * 100).toFixed(2)) : 0,
|
|
216
|
+
avgWin: winLoss.wins > 0 ? Number((winLoss.profit / winLoss.wins).toFixed(2)) : 0,
|
|
217
|
+
avgLoss: winLoss.losses > 0 ? Number((winLoss.loss / winLoss.losses).toFixed(2)) : 0,
|
|
218
|
+
profitFactor: winLoss.losses !== 0 ? Number((Math.abs(winLoss.profit / winLoss.loss)).toFixed(2)) : (winLoss.profit > 0 ? Infinity : 0),
|
|
219
|
+
dailyPnL: Array.from(dailyPnL.entries()).map(([date, pnl]) => ({ date, pnl: Number(pnl.toFixed(2)) })).sort((a,b) => a.date.localeCompare(b.date)).slice(-20)
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// ==========================================================================================
|
|
223
|
+
// 4. PORTFOLIO & COPIED PIs (The Core Requirement)
|
|
224
|
+
// ==========================================================================================
|
|
225
|
+
if (currentPortfolio) {
|
|
226
|
+
const pData = rules.portfolio.extractPortfolioData(currentPortfolio);
|
|
227
|
+
const positions = rules.portfolio.extractPositions(pData);
|
|
228
|
+
|
|
229
|
+
// Extract Mirrors using Rules (assumes rule exists, or manual extraction if needed)
|
|
230
|
+
// If rules.portfolio doesn't have extractMirrors, we can look at pData.AggregatedMirrors directly
|
|
231
|
+
const mirrors = pData.AggregatedMirrors || [];
|
|
232
|
+
|
|
233
|
+
let totalInv = 0, totalProf = 0;
|
|
234
|
+
const secExp = {}, assetExp = {}, secProfits = {};
|
|
235
|
+
|
|
236
|
+
// A. Process Direct Positions
|
|
237
|
+
positions.forEach(pos => {
|
|
238
|
+
const id = rules.portfolio.getInstrumentId(pos);
|
|
239
|
+
const inv = rules.portfolio.getInvested(pos);
|
|
240
|
+
const profPct = rules.portfolio.getNetProfit(pos);
|
|
241
|
+
const profVal = inv * (profPct / 100);
|
|
242
|
+
|
|
243
|
+
totalInv += inv;
|
|
244
|
+
totalProf += profVal;
|
|
245
|
+
|
|
246
|
+
const ticker = resolveTicker(id);
|
|
247
|
+
const sector = resolveSector(id);
|
|
248
|
+
|
|
249
|
+
secExp[sector] = (secExp[sector] || 0) + inv;
|
|
250
|
+
assetExp[ticker] = (assetExp[ticker] || 0) + inv;
|
|
251
|
+
|
|
252
|
+
if (!secProfits[sector]) secProfits[sector] = { profit: 0, weight: 0 };
|
|
253
|
+
secProfits[sector].profit += profVal;
|
|
254
|
+
secProfits[sector].weight += inv;
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// B. Process Copied PIs (Mirrors)
|
|
258
|
+
// * We iterate the USER'S mirrors, then lookup the PI data.
|
|
259
|
+
// * This avoids fetching all PIs as primary data.
|
|
260
|
+
mirrors.forEach(m => {
|
|
261
|
+
const cid = String(m.ParentCID || m.CID); // Adjust based on exact schema
|
|
262
|
+
const inv = m.Invested || m.Amount || 0;
|
|
263
|
+
const profPct = m.NetProfit || 0;
|
|
264
|
+
|
|
265
|
+
totalInv += inv;
|
|
266
|
+
totalProf += (inv * (profPct / 100));
|
|
267
|
+
|
|
268
|
+
// LOOKUP: Get PI details from the global reference data
|
|
269
|
+
const rank = globalRankings.find(r => String(r.pi_id || r.CustomerId) === cid);
|
|
270
|
+
|
|
271
|
+
result.copiedPIs.data.push({
|
|
272
|
+
cid,
|
|
273
|
+
username: resolveUsername(cid),
|
|
274
|
+
invested: inv,
|
|
275
|
+
netProfit: m.NetProfit || 0,
|
|
276
|
+
value: m.Value || 0,
|
|
277
|
+
pendingClosure: m.PendingForClosure === true,
|
|
278
|
+
isRanked: !!rank,
|
|
279
|
+
// Enrich with Reference Data
|
|
280
|
+
rankData: rank ? {
|
|
281
|
+
riskScore: rules.rankings.getRiskScore(rank),
|
|
282
|
+
gain: rules.rankings.getTotalGain(rank),
|
|
283
|
+
aum: rules.rankings.getAUMTier(rank)
|
|
284
|
+
} : null
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Summary Stats
|
|
289
|
+
result.portfolioSummary = {
|
|
290
|
+
totalInvested: Number(totalInv.toFixed(2)),
|
|
291
|
+
totalProfit: Number(totalProf.toFixed(2)),
|
|
292
|
+
profitPercent: totalInv > 0 ? Number(((totalProf / totalInv) * 100).toFixed(2)) : 0
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// Calculate Best/Worst Sectors
|
|
296
|
+
let bestS = null, worstS = null, bestP = -Infinity, worstP = Infinity;
|
|
297
|
+
Object.entries(secProfits).forEach(([sec, d]) => {
|
|
298
|
+
if (d.weight <= 0) return;
|
|
299
|
+
const p = (d.profit / d.weight) * 100;
|
|
300
|
+
if (p > bestP) { bestP = p; bestS = sec; }
|
|
301
|
+
if (p < worstP) { worstP = p; worstS = sec; }
|
|
302
|
+
});
|
|
303
|
+
result.sectorPerformance = {
|
|
304
|
+
bestSector: bestS,
|
|
305
|
+
worstSector: worstS,
|
|
306
|
+
bestSectorProfit: bestS ? Number(bestP.toFixed(2)) : 0,
|
|
307
|
+
worstSectorProfit: worstS ? Number(worstP.toFixed(2)) : 0
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// Exposure Charts
|
|
311
|
+
if (totalInv > 0) {
|
|
312
|
+
Object.keys(secExp).forEach(k => result.sectorExposure.data[k] = Number(((secExp[k]/totalInv)*100).toFixed(2)));
|
|
313
|
+
Object.entries(assetExp)
|
|
314
|
+
.sort((a,b) => b[1] - a[1])
|
|
315
|
+
.slice(0, 10)
|
|
316
|
+
.forEach(([k,v]) => result.assetExposure.data[k] = Number(((v/totalInv)*100).toFixed(2)));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
this.setResult(entityId, result);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
module.exports = SignedInUserProfileMetrics;
|