bulltrackers-module 1.0.768 → 1.0.770
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 +557 -337
- package/functions/computation-system-v2/computations/GlobalAumPerAsset30D.js +103 -0
- 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/RiskScoreIncrease.js +13 -13
- 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 +30 -128
- 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/data/DataFetcher.js +250 -184
- 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 +215 -129
- 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 +105 -67
- package/functions/computation-system-v2/framework/testing/ComputationTester.js +12 -6
- package/functions/computation-system-v2/handlers/dispatcher.js +57 -29
- package/functions/computation-system-v2/handlers/scheduler.js +172 -203
- 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-invalidation-scenarios.js +234 -0
- package/package.json +1 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Aggregates $ AUM per asset across ALL Popular Investors.
|
|
3
|
+
* * STRATEGY: Global Computation (Reduce Step).
|
|
4
|
+
* * INPUT: Reads 'computation_results' for 'pidailyassetaum'.
|
|
5
|
+
* * OUTPUT: List of { symbol, amount } for Materialized View generation.
|
|
6
|
+
*/
|
|
7
|
+
const { Computation } = require('../framework');
|
|
8
|
+
|
|
9
|
+
class GlobalAumPerAsset extends Computation {
|
|
10
|
+
|
|
11
|
+
static getConfig() {
|
|
12
|
+
return {
|
|
13
|
+
name: 'GlobalAumPerAsset',
|
|
14
|
+
type: 'global',
|
|
15
|
+
category: 'market_insights',
|
|
16
|
+
|
|
17
|
+
requires: {
|
|
18
|
+
'computation_results': {
|
|
19
|
+
lookback: 0,
|
|
20
|
+
mandatory: true,
|
|
21
|
+
fields: ['result_data', 'computation_name', 'date'],
|
|
22
|
+
// Lowercase to match BigQuery storage
|
|
23
|
+
filter: {
|
|
24
|
+
computation_name: 'pidailyassetaum'
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
dependencies: ['PIDailyAssetAUM'],
|
|
30
|
+
|
|
31
|
+
storage: {
|
|
32
|
+
bigquery: true,
|
|
33
|
+
firestore: {
|
|
34
|
+
enabled: true,
|
|
35
|
+
path: 'global_metrics/aum_per_asset_{date}',
|
|
36
|
+
merge: true
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async process(context) {
|
|
43
|
+
const { data } = context;
|
|
44
|
+
|
|
45
|
+
let rows = data['computation_results'];
|
|
46
|
+
if (!rows) {
|
|
47
|
+
console.log('[GlobalAumPerAsset] No input data found.');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!Array.isArray(rows) && typeof rows === 'object') {
|
|
52
|
+
rows = Object.values(rows);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (rows.length === 0) {
|
|
56
|
+
console.log('[GlobalAumPerAsset] Input array is empty.');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log(`[GlobalAumPerAsset] Aggregating ${rows.length} PI portfolios...`);
|
|
61
|
+
|
|
62
|
+
const globalTotals = new Map();
|
|
63
|
+
let piCount = 0;
|
|
64
|
+
|
|
65
|
+
for (const row of rows) {
|
|
66
|
+
let assetMap = row.result_data;
|
|
67
|
+
|
|
68
|
+
if (typeof assetMap === 'string') {
|
|
69
|
+
try { assetMap = JSON.parse(assetMap); } catch (e) { continue; }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!assetMap) continue;
|
|
73
|
+
|
|
74
|
+
piCount++;
|
|
75
|
+
|
|
76
|
+
for (const [ticker, value] of Object.entries(assetMap)) {
|
|
77
|
+
if (typeof value === 'number') {
|
|
78
|
+
const current = globalTotals.get(ticker) || 0;
|
|
79
|
+
globalTotals.set(ticker, current + value);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const materializedViewData = [];
|
|
85
|
+
for (const [symbol, amount] of globalTotals.entries()) {
|
|
86
|
+
if (amount > 0) {
|
|
87
|
+
materializedViewData.push({
|
|
88
|
+
symbol: symbol,
|
|
89
|
+
amount: Number(amount.toFixed(2))
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
materializedViewData.sort((a, b) => b.amount - a.amount);
|
|
95
|
+
|
|
96
|
+
console.log(`[GlobalAumPerAsset] Generated stats for ${materializedViewData.length} unique assets from ${piCount} PIs.`);
|
|
97
|
+
|
|
98
|
+
// FIX: Use '_global' to match Orchestrator context (was 'global')
|
|
99
|
+
this.setResult('_global', materializedViewData);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = GlobalAumPerAsset;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const { Computation } = require('../framework');
|
|
2
|
+
|
|
3
|
+
class PIDailyAssetAUM extends Computation {
|
|
4
|
+
|
|
5
|
+
static getConfig() {
|
|
6
|
+
return {
|
|
7
|
+
name: 'PIDailyAssetAUM',
|
|
8
|
+
type: 'per-entity',
|
|
9
|
+
category: 'popular_investor',
|
|
10
|
+
isHistorical: true,
|
|
11
|
+
|
|
12
|
+
requires: {
|
|
13
|
+
// Driver: Only process Popular Investors
|
|
14
|
+
'portfolio_snapshots': {
|
|
15
|
+
lookback: 30, // Extended lookback to ensure we find snapshots
|
|
16
|
+
mandatory: true,
|
|
17
|
+
fields: ['user_id', 'portfolio_data', 'date', 'user_type'],
|
|
18
|
+
filter: { user_type: 'POPULAR_INVESTOR' }
|
|
19
|
+
},
|
|
20
|
+
'pi_rankings': {
|
|
21
|
+
lookback: 30, // Extended lookback to match portfolios
|
|
22
|
+
mandatory: true,
|
|
23
|
+
fields: ['pi_id', 'rankings_data', 'date']
|
|
24
|
+
},
|
|
25
|
+
'ticker_mappings': {
|
|
26
|
+
mandatory: false,
|
|
27
|
+
fields: ['instrument_id', 'ticker']
|
|
28
|
+
},
|
|
29
|
+
'pi_master_list': {
|
|
30
|
+
mandatory: false,
|
|
31
|
+
fields: ['cid', 'username']
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
storage: {
|
|
36
|
+
bigquery: true,
|
|
37
|
+
firestore: {
|
|
38
|
+
enabled: true,
|
|
39
|
+
path: 'popular_investors/{entityId}/metrics/asset_aum_{date}',
|
|
40
|
+
merge: true
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async process(context) {
|
|
47
|
+
const { data, entityId, rules } = context;
|
|
48
|
+
|
|
49
|
+
// --- HELPER: Safe Date Parser ---
|
|
50
|
+
// Handles BigQuery { value: '2023-01-01' } objects to prevent NaN errors
|
|
51
|
+
const parseDate = (d) => {
|
|
52
|
+
if (!d) return null;
|
|
53
|
+
if (d instanceof Date) return d;
|
|
54
|
+
if (typeof d === 'object' && d.value) return new Date(d.value);
|
|
55
|
+
return new Date(d);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const getEntityRows = (dataset) => {
|
|
59
|
+
if (!dataset) return [];
|
|
60
|
+
if (dataset[entityId]) return Array.isArray(dataset[entityId]) ? dataset[entityId] : [dataset[entityId]];
|
|
61
|
+
// Handle mismatched ID columns (user_id vs pi_id)
|
|
62
|
+
if (Array.isArray(dataset)) return dataset.filter(r => String(r.user_id || r.pi_id || r.cid) === String(entityId));
|
|
63
|
+
return [];
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Mappings
|
|
67
|
+
const tickerMap = new Map();
|
|
68
|
+
const mappingsList = Array.isArray(context.data['ticker_mappings']) ? context.data['ticker_mappings'] : Object.values(context.data['ticker_mappings'] || {});
|
|
69
|
+
mappingsList.forEach(row => { if (row.instrument_id) tickerMap.set(Number(row.instrument_id), row.ticker); });
|
|
70
|
+
const resolveTicker = (id) => tickerMap.get(Number(id)) || `ID:${id}`;
|
|
71
|
+
|
|
72
|
+
// 2. Get Valid Snapshots
|
|
73
|
+
const portfolios = getEntityRows(data['portfolio_snapshots']);
|
|
74
|
+
const rankings = getEntityRows(data['pi_rankings']);
|
|
75
|
+
|
|
76
|
+
let validPortfolio = null;
|
|
77
|
+
let validRanking = null;
|
|
78
|
+
|
|
79
|
+
// Sort descending using safe date parsing
|
|
80
|
+
portfolios.sort((a,b) => parseDate(b.date) - parseDate(a.date));
|
|
81
|
+
rankings.sort((a,b) => parseDate(b.date) - parseDate(a.date));
|
|
82
|
+
|
|
83
|
+
if (portfolios.length > 0) {
|
|
84
|
+
validPortfolio = portfolios[0];
|
|
85
|
+
const pDate = parseDate(validPortfolio.date);
|
|
86
|
+
|
|
87
|
+
validRanking = rankings.find(r => {
|
|
88
|
+
const rDate = parseDate(r.date);
|
|
89
|
+
const diffTime = Math.abs(pDate - rDate);
|
|
90
|
+
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
91
|
+
|
|
92
|
+
// Allow up to 14 days gap (matches your data reality)
|
|
93
|
+
return diffDays <= 14;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!validPortfolio || !validRanking) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 3. Calculation
|
|
102
|
+
const pData = rules.portfolio.extractPortfolioData(validPortfolio);
|
|
103
|
+
const rData = rules.rankings.extractRankingsData(validRanking);
|
|
104
|
+
|
|
105
|
+
// FIX: Use 'getAUM' (checked from rules/rankings.js)
|
|
106
|
+
const totalAum = rules.rankings.getAUM(rData);
|
|
107
|
+
|
|
108
|
+
if (!totalAum || totalAum <= 0) return;
|
|
109
|
+
|
|
110
|
+
const positions = rules.portfolio.extractPositions(pData);
|
|
111
|
+
const assetAumMap = {};
|
|
112
|
+
|
|
113
|
+
positions.forEach(pos => {
|
|
114
|
+
const id = rules.portfolio.getInstrumentId(pos);
|
|
115
|
+
const ticker = resolveTicker(id);
|
|
116
|
+
|
|
117
|
+
// FIX: Use 'getInvested' (checked from rules/portfolio.js)
|
|
118
|
+
const weight = rules.portfolio.getInvested(pos);
|
|
119
|
+
|
|
120
|
+
if (weight > 0) {
|
|
121
|
+
// weight is a percentage (e.g., 5.5 for 5.5%), so divide by 100
|
|
122
|
+
const dollarValue = (weight / 100) * totalAum;
|
|
123
|
+
assetAumMap[ticker] = (assetAumMap[ticker] || 0) + dollarValue;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// 4. Formatting
|
|
128
|
+
Object.keys(assetAumMap).forEach(k => assetAumMap[k] = Number(assetAumMap[k].toFixed(2)));
|
|
129
|
+
|
|
130
|
+
this.setResult(entityId, assetAumMap);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = PIDailyAssetAUM;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview PI Feature Vectors
|
|
3
|
+
*
|
|
4
|
+
* Global computation that builds normalized feature vectors for all Popular Investors.
|
|
5
|
+
* Used by PiRecommender to find balanced recommendations.
|
|
6
|
+
*
|
|
7
|
+
* Features computed per PI:
|
|
8
|
+
* - Sector exposure (normalized %)
|
|
9
|
+
* - Risk profile (volatility, risk score)
|
|
10
|
+
* - Performance metrics (gain %, win ratio)
|
|
11
|
+
* - Quality indicators (AUM tier, copiers)
|
|
12
|
+
*/
|
|
13
|
+
const { Computation } = require('../framework');
|
|
14
|
+
|
|
15
|
+
class PiFeatureVectors extends Computation {
|
|
16
|
+
static getConfig() {
|
|
17
|
+
return {
|
|
18
|
+
name: 'PiFeatureVectors',
|
|
19
|
+
description: 'Computes feature vectors for all PIs for recommendation matching',
|
|
20
|
+
type: 'global',
|
|
21
|
+
category: 'popular_investor',
|
|
22
|
+
isHistorical: false,
|
|
23
|
+
|
|
24
|
+
requires: {
|
|
25
|
+
'portfolio_snapshots': {
|
|
26
|
+
lookback: 0,
|
|
27
|
+
mandatory: true,
|
|
28
|
+
fields: ['user_id', 'portfolio_data', 'date'],
|
|
29
|
+
filter: { user_type: 'POPULAR_INVESTOR' }
|
|
30
|
+
},
|
|
31
|
+
'pi_rankings': {
|
|
32
|
+
lookback: 1,
|
|
33
|
+
mandatory: true,
|
|
34
|
+
fields: ['pi_id', 'rankings_data', 'username', 'date']
|
|
35
|
+
},
|
|
36
|
+
'pi_master_list': {
|
|
37
|
+
lookback: 1,
|
|
38
|
+
mandatory: false,
|
|
39
|
+
fields: ['cid', 'username', 'verified_date']
|
|
40
|
+
},
|
|
41
|
+
'ticker_mappings': { mandatory: false },
|
|
42
|
+
'sector_mappings': { mandatory: false }
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
storage: {
|
|
46
|
+
bigquery: true,
|
|
47
|
+
firestore: { enabled: false }
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async process(context) {
|
|
53
|
+
const { data, rules, targetDate } = context;
|
|
54
|
+
|
|
55
|
+
// ===========================================================================
|
|
56
|
+
// SETUP: Helpers and reference data
|
|
57
|
+
// ===========================================================================
|
|
58
|
+
const toArray = (input) => {
|
|
59
|
+
if (!input) return [];
|
|
60
|
+
if (Array.isArray(input)) return input;
|
|
61
|
+
// Handle Map-like structure from DataFetcher
|
|
62
|
+
return Object.values(input).flat();
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Build sector lookup
|
|
66
|
+
const tickerMap = new Map();
|
|
67
|
+
toArray(data['ticker_mappings']).forEach(row => {
|
|
68
|
+
if (row.instrument_id && row.ticker) {
|
|
69
|
+
tickerMap.set(Number(row.instrument_id), row.ticker.toUpperCase());
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const sectorMap = new Map();
|
|
74
|
+
toArray(data['sector_mappings']).forEach(row => {
|
|
75
|
+
if (row.symbol && row.sector) {
|
|
76
|
+
sectorMap.set(row.symbol.toUpperCase(), row.sector);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const resolveSector = (instrumentId) => {
|
|
81
|
+
const ticker = tickerMap.get(Number(instrumentId));
|
|
82
|
+
return ticker ? (sectorMap.get(ticker) || 'Other') : 'Other';
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// All known sectors for consistent vector dimensions
|
|
86
|
+
const ALL_SECTORS = [
|
|
87
|
+
'Technology', 'Healthcare', 'Financial Services', 'Consumer Cyclical',
|
|
88
|
+
'Industrials', 'Energy', 'Utilities', 'Real Estate', 'Communication Services',
|
|
89
|
+
'Consumer Defensive', 'Basic Materials', 'Crypto', 'Commodities', 'ETF', 'Other'
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
// ===========================================================================
|
|
93
|
+
// GATHER: PI portfolio and rankings data
|
|
94
|
+
// ===========================================================================
|
|
95
|
+
const rankings = toArray(data['pi_rankings']);
|
|
96
|
+
const portfolios = data['portfolio_snapshots']; // Map { piId: [rows] }
|
|
97
|
+
const masterList = toArray(data['pi_master_list']);
|
|
98
|
+
|
|
99
|
+
// Build username lookup
|
|
100
|
+
const usernameMap = new Map();
|
|
101
|
+
rankings.forEach(r => {
|
|
102
|
+
const id = String(r.pi_id || r.CustomerId);
|
|
103
|
+
usernameMap.set(id, r.username || r.UserName);
|
|
104
|
+
});
|
|
105
|
+
masterList.forEach(m => {
|
|
106
|
+
if (m.cid && m.username) usernameMap.set(String(m.cid), m.username);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ===========================================================================
|
|
110
|
+
// COMPUTE: Feature vectors for each PI
|
|
111
|
+
// ===========================================================================
|
|
112
|
+
const featureVectors = {};
|
|
113
|
+
|
|
114
|
+
// Process each PI's portfolio
|
|
115
|
+
const piIds = Object.keys(portfolios || {});
|
|
116
|
+
|
|
117
|
+
for (const piId of piIds) {
|
|
118
|
+
const rows = Array.isArray(portfolios[piId]) ? portfolios[piId] : [portfolios[piId]];
|
|
119
|
+
const latestRow = rows[rows.length - 1];
|
|
120
|
+
if (!latestRow) continue;
|
|
121
|
+
|
|
122
|
+
// Extract positions
|
|
123
|
+
const pData = rules.portfolio.extractPortfolioData(latestRow);
|
|
124
|
+
const positions = rules.portfolio.extractPositions(pData);
|
|
125
|
+
|
|
126
|
+
if (positions.length === 0) continue;
|
|
127
|
+
|
|
128
|
+
// Calculate sector exposure
|
|
129
|
+
const sectorWeights = {};
|
|
130
|
+
let totalValue = 0;
|
|
131
|
+
|
|
132
|
+
positions.forEach(pos => {
|
|
133
|
+
const instrumentId = rules.portfolio.getInstrumentId(pos);
|
|
134
|
+
const invested = rules.portfolio.getInvested(pos) || 0;
|
|
135
|
+
const sector = resolveSector(instrumentId);
|
|
136
|
+
|
|
137
|
+
sectorWeights[sector] = (sectorWeights[sector] || 0) + invested;
|
|
138
|
+
totalValue += invested;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Normalize sector exposure to percentages
|
|
142
|
+
const sectorVector = {};
|
|
143
|
+
ALL_SECTORS.forEach(s => {
|
|
144
|
+
sectorVector[s] = totalValue > 0
|
|
145
|
+
? Number(((sectorWeights[s] || 0) / totalValue).toFixed(4))
|
|
146
|
+
: 0;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Get ranking data
|
|
150
|
+
if (piIds.indexOf(piId) === 0) {
|
|
151
|
+
const availableIds = rankings.map(r => r.pi_id || 'undefined').join(',');
|
|
152
|
+
console.log(`[DEBUG] Available IDs in rankings: ${availableIds}`);
|
|
153
|
+
console.log(`[DEBUG] First ranking row keys: ${Object.keys(rankings[0] || {}).join(',')}`);
|
|
154
|
+
if (rankings[0]) console.log(`[DEBUG] First ranking row pi_id: ${rankings[0].pi_id}`);
|
|
155
|
+
}
|
|
156
|
+
const rankRow = rankings.find(r => String(r.pi_id || r.CustomerId) === String(piId));
|
|
157
|
+
|
|
158
|
+
if (piIds.indexOf(piId) === 0) {
|
|
159
|
+
console.log(`[DEBUG] PiId: ${piId}`);
|
|
160
|
+
console.log(`[DEBUG] RankRow found: ${!!rankRow}`);
|
|
161
|
+
if (rankRow) {
|
|
162
|
+
console.log(`[DEBUG] RankRow keys: ${Object.keys(rankRow).join(',')}`);
|
|
163
|
+
console.log(`[DEBUG] RankingsData type: ${typeof rankRow.rankings_data}`);
|
|
164
|
+
if (typeof rankRow.rankings_data === 'object') {
|
|
165
|
+
console.log(`[DEBUG] RankingsData keys: ${Object.keys(rankRow.rankings_data).join(',')}`);
|
|
166
|
+
console.log(`[DEBUG] RankingsData.Gain: ${rankRow.rankings_data.Gain}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let riskScore = 5, gain = 0, copiers = 0, winRatio = 0, aumTier = 0;
|
|
172
|
+
|
|
173
|
+
if (rankRow) {
|
|
174
|
+
const rData = rules.rankings.extractRankingsData(rankRow);
|
|
175
|
+
if (rData) {
|
|
176
|
+
riskScore = rules.rankings.getRiskScore(rData) || 5;
|
|
177
|
+
gain = rules.rankings.getTotalGain(rData) || 0;
|
|
178
|
+
copiers = rules.rankings.getCopiers(rData) || 0;
|
|
179
|
+
winRatio = rules.rankings.getWinRatio(rData) || 0;
|
|
180
|
+
aumTier = rules.rankings.getAUMTier(rData) || 0;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Calculate concentration (Herfindahl index) - higher = more concentrated
|
|
185
|
+
const weights = Object.values(sectorWeights).filter(w => w > 0);
|
|
186
|
+
const hhi = totalValue > 0
|
|
187
|
+
? weights.reduce((sum, w) => sum + Math.pow(w / totalValue, 2), 0)
|
|
188
|
+
: 1;
|
|
189
|
+
|
|
190
|
+
// Assemble feature vector
|
|
191
|
+
featureVectors[piId] = {
|
|
192
|
+
piId: String(piId),
|
|
193
|
+
username: usernameMap.get(String(piId)) || 'Unknown',
|
|
194
|
+
|
|
195
|
+
// Sector exposure (normalized)
|
|
196
|
+
sectors: sectorVector,
|
|
197
|
+
|
|
198
|
+
// Risk profile
|
|
199
|
+
riskScore: riskScore,
|
|
200
|
+
concentration: Number(hhi.toFixed(4)), // 0-1, higher = less diversified
|
|
201
|
+
|
|
202
|
+
// Performance
|
|
203
|
+
gain: gain,
|
|
204
|
+
winRatio: winRatio,
|
|
205
|
+
|
|
206
|
+
// Quality
|
|
207
|
+
copiers: copiers,
|
|
208
|
+
aumTier: aumTier,
|
|
209
|
+
positionCount: positions.length,
|
|
210
|
+
|
|
211
|
+
// Metadata
|
|
212
|
+
totalValue: Number(totalValue.toFixed(2)),
|
|
213
|
+
computedAt: targetDate
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Store all vectors as a single global result
|
|
218
|
+
this.setResult('_global', {
|
|
219
|
+
vectors: featureVectors,
|
|
220
|
+
piCount: Object.keys(featureVectors).length,
|
|
221
|
+
sectorDimensions: ALL_SECTORS,
|
|
222
|
+
computedAt: targetDate
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = PiFeatureVectors;
|