bulltrackers-module 1.0.765 → 1.0.768
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/computations/BehavioralAnomaly.js +298 -186
- 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/PopularInvestorProfileMetrics.js +354 -641
- package/functions/computation-system-v2/config/bulltrackers.config.js +26 -14
- 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 +142 -4
- package/functions/computation-system-v2/framework/execution/Orchestrator.js +119 -122
- package/functions/computation-system-v2/framework/storage/StorageManager.js +16 -18
- package/functions/computation-system-v2/framework/testing/ComputationTester.js +155 -66
- package/functions/computation-system-v2/handlers/scheduler.js +15 -5
- package/functions/computation-system-v2/scripts/test-computation-dag.js +109 -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,294 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Popular Investor Risk Metrics Computation
|
|
3
|
-
*
|
|
4
|
-
* Calculates comprehensive risk metrics for each popular investor:
|
|
5
|
-
* - Sharpe Ratio (risk-adjusted returns)
|
|
6
|
-
* - Sortino Ratio (downside risk)
|
|
7
|
-
* - Max Drawdown
|
|
8
|
-
* - Win Ratio
|
|
9
|
-
* - Portfolio concentration
|
|
10
|
-
*
|
|
11
|
-
* This computation demonstrates:
|
|
12
|
-
* - Using both metrics and portfolio rules
|
|
13
|
-
* - Per-entity computation pattern
|
|
14
|
-
* - Dual storage (BigQuery + Firestore)
|
|
15
|
-
* - User-centric Firestore path
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
const { Computation } = require('../framework');
|
|
19
|
-
|
|
20
|
-
class PopularInvestorRiskMetrics extends Computation {
|
|
21
|
-
static getConfig() {
|
|
22
|
-
return {
|
|
23
|
-
name: 'PopularInvestorRiskMetrics',
|
|
24
|
-
category: 'risk_analytics',
|
|
25
|
-
|
|
26
|
-
// Data requirements
|
|
27
|
-
requires: {
|
|
28
|
-
'portfolio_snapshots': {
|
|
29
|
-
lookback: 30, // Need 30 days of history for risk metrics
|
|
30
|
-
mandatory: true,
|
|
31
|
-
filter: { user_type: 'POPULAR_INVESTOR' }
|
|
32
|
-
},
|
|
33
|
-
'trade_history_snapshots': {
|
|
34
|
-
lookback: 30,
|
|
35
|
-
mandatory: false // Optional, used for trade-based metrics
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
|
|
39
|
-
// No computation dependencies
|
|
40
|
-
dependencies: [],
|
|
41
|
-
|
|
42
|
-
// Per-entity means we process each user separately
|
|
43
|
-
type: 'per-entity',
|
|
44
|
-
|
|
45
|
-
// Keep 90 days of history
|
|
46
|
-
ttlDays: 90,
|
|
47
|
-
|
|
48
|
-
// Schedule: Run daily at 3 AM
|
|
49
|
-
schedule: {
|
|
50
|
-
frequency: 'daily',
|
|
51
|
-
time: '03:00'
|
|
52
|
-
},
|
|
53
|
-
|
|
54
|
-
// =========================================================================
|
|
55
|
-
// STORAGE: Write to both BigQuery and Firestore
|
|
56
|
-
// =========================================================================
|
|
57
|
-
storage: {
|
|
58
|
-
// BigQuery: for analytics queries and as dependency for other computations
|
|
59
|
-
bigquery: true,
|
|
60
|
-
|
|
61
|
-
// Firestore: for real-time frontend access
|
|
62
|
-
// Each user gets their risk metrics in their user document
|
|
63
|
-
firestore: {
|
|
64
|
-
enabled: true,
|
|
65
|
-
path: '/users/{entityId}/computations/risk_metrics',
|
|
66
|
-
merge: false, // Overwrite the entire document
|
|
67
|
-
includeMetadata: true // Include _computedAt, _codeHash, etc.
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
static getSchema() {
|
|
74
|
-
return {
|
|
75
|
-
type: 'object',
|
|
76
|
-
properties: {
|
|
77
|
-
userId: { type: 'string' },
|
|
78
|
-
date: { type: 'string' },
|
|
79
|
-
|
|
80
|
-
// Portfolio overview
|
|
81
|
-
portfolioValue: { type: 'number' },
|
|
82
|
-
positionCount: { type: 'number' },
|
|
83
|
-
|
|
84
|
-
// Risk metrics
|
|
85
|
-
sharpeRatio: { type: 'number' },
|
|
86
|
-
sortinoRatio: { type: 'number' },
|
|
87
|
-
maxDrawdown: { type: 'number' },
|
|
88
|
-
valueAtRisk95: { type: 'number' },
|
|
89
|
-
|
|
90
|
-
// Performance metrics
|
|
91
|
-
winRatio: { type: 'number' },
|
|
92
|
-
avgDailyReturn: { type: 'number' },
|
|
93
|
-
returnVolatility: { type: 'number' },
|
|
94
|
-
|
|
95
|
-
// Concentration metrics
|
|
96
|
-
top3Concentration: { type: 'number' },
|
|
97
|
-
|
|
98
|
-
// Risk grade (A-F)
|
|
99
|
-
riskGrade: { type: 'string' }
|
|
100
|
-
},
|
|
101
|
-
required: ['userId', 'date', 'riskGrade']
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
static getWeight() {
|
|
106
|
-
return 2.0; // Higher weight = more expensive
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Process a single investor's risk metrics.
|
|
111
|
-
*/
|
|
112
|
-
async process(context) {
|
|
113
|
-
const { date, entityId, data, rules } = context;
|
|
114
|
-
|
|
115
|
-
const portfolioHistory = data['portfolio_snapshots'];
|
|
116
|
-
|
|
117
|
-
// Need portfolio history to calculate risk metrics
|
|
118
|
-
if (!portfolioHistory || !Array.isArray(portfolioHistory) || portfolioHistory.length === 0) {
|
|
119
|
-
this.log('DEBUG', `No portfolio history for user ${entityId}`);
|
|
120
|
-
this.setResult(entityId, this._emptyResult(entityId, date));
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Sort by date ascending
|
|
125
|
-
const sortedHistory = [...portfolioHistory].sort((a, b) =>
|
|
126
|
-
new Date(a.date || a.snapshot_date) - new Date(b.date || b.snapshot_date)
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
// =====================================================================
|
|
130
|
-
// STEP 1: Extract daily portfolio values and calculate returns
|
|
131
|
-
// =====================================================================
|
|
132
|
-
|
|
133
|
-
const dailyValues = sortedHistory.map(snapshot => {
|
|
134
|
-
const positions = rules.portfolio.extractPositions(snapshot);
|
|
135
|
-
return rules.portfolio.calculateTotalValue(positions);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
// Calculate daily returns (percentage change)
|
|
139
|
-
const dailyReturns = [];
|
|
140
|
-
for (let i = 1; i < dailyValues.length; i++) {
|
|
141
|
-
if (dailyValues[i - 1] > 0) {
|
|
142
|
-
const returnPct = ((dailyValues[i] - dailyValues[i - 1]) / dailyValues[i - 1]) * 100;
|
|
143
|
-
dailyReturns.push(returnPct);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Need at least a few returns for meaningful metrics
|
|
148
|
-
if (dailyReturns.length < 5) {
|
|
149
|
-
this.log('DEBUG', `Insufficient return history for user ${entityId}: ${dailyReturns.length} days`);
|
|
150
|
-
this.setResult(entityId, this._emptyResult(entityId, date));
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// =====================================================================
|
|
155
|
-
// STEP 2: Calculate risk metrics using rules.metrics
|
|
156
|
-
// =====================================================================
|
|
157
|
-
|
|
158
|
-
// Sharpe Ratio (risk-free rate = 4% annualized)
|
|
159
|
-
const sharpeRatio = rules.metrics.calculateSharpeRatio(dailyReturns, 4, 252);
|
|
160
|
-
|
|
161
|
-
// Sortino Ratio (target return = 0)
|
|
162
|
-
const sortinoRatio = rules.metrics.calculateSortinoRatio(dailyReturns, 0, 252);
|
|
163
|
-
|
|
164
|
-
// Max Drawdown
|
|
165
|
-
const { maxDrawdown } = rules.metrics.calculateMaxDrawdown(dailyValues);
|
|
166
|
-
|
|
167
|
-
// Value at Risk (95% confidence)
|
|
168
|
-
const valueAtRisk95 = rules.metrics.calculateVaR(dailyReturns, 0.95);
|
|
169
|
-
|
|
170
|
-
// Basic statistics
|
|
171
|
-
const avgDailyReturn = rules.metrics.mean(dailyReturns);
|
|
172
|
-
const returnVolatility = rules.metrics.standardDeviation(dailyReturns);
|
|
173
|
-
|
|
174
|
-
// =====================================================================
|
|
175
|
-
// STEP 3: Calculate portfolio metrics using rules.portfolio
|
|
176
|
-
// =====================================================================
|
|
177
|
-
|
|
178
|
-
// Get latest portfolio snapshot
|
|
179
|
-
const latestSnapshot = sortedHistory[sortedHistory.length - 1];
|
|
180
|
-
const positions = rules.portfolio.extractPositions(latestSnapshot);
|
|
181
|
-
|
|
182
|
-
const portfolioValue = rules.portfolio.calculateTotalValue(positions);
|
|
183
|
-
const positionCount = positions.length;
|
|
184
|
-
const winRatio = rules.portfolio.calculateWinRatio(positions);
|
|
185
|
-
|
|
186
|
-
// Calculate concentration (top 3 positions as % of portfolio)
|
|
187
|
-
const top3 = rules.portfolio.getTopPositions(positions, 3);
|
|
188
|
-
const top3Value = top3.reduce((sum, pos) => sum + rules.portfolio.getValue(pos), 0);
|
|
189
|
-
const top3Concentration = portfolioValue > 0 ? (top3Value / portfolioValue) * 100 : 0;
|
|
190
|
-
|
|
191
|
-
// =====================================================================
|
|
192
|
-
// STEP 4: Calculate risk grade (A-F based on combined metrics)
|
|
193
|
-
// =====================================================================
|
|
194
|
-
|
|
195
|
-
const riskGrade = this._calculateRiskGrade({
|
|
196
|
-
sharpeRatio,
|
|
197
|
-
sortinoRatio,
|
|
198
|
-
maxDrawdown,
|
|
199
|
-
winRatio,
|
|
200
|
-
top3Concentration
|
|
201
|
-
}, rules);
|
|
202
|
-
|
|
203
|
-
// =====================================================================
|
|
204
|
-
// STEP 5: Build and store result
|
|
205
|
-
// =====================================================================
|
|
206
|
-
|
|
207
|
-
this.setResult(entityId, {
|
|
208
|
-
userId: entityId,
|
|
209
|
-
date,
|
|
210
|
-
|
|
211
|
-
// Portfolio overview
|
|
212
|
-
portfolioValue: rules.metrics.round(portfolioValue, 2),
|
|
213
|
-
positionCount,
|
|
214
|
-
|
|
215
|
-
// Risk metrics (annualized where applicable)
|
|
216
|
-
sharpeRatio: rules.metrics.round(sharpeRatio, 3),
|
|
217
|
-
sortinoRatio: rules.metrics.round(sortinoRatio, 3),
|
|
218
|
-
maxDrawdown: rules.metrics.round(maxDrawdown, 2),
|
|
219
|
-
valueAtRisk95: rules.metrics.round(valueAtRisk95, 2),
|
|
220
|
-
|
|
221
|
-
// Performance
|
|
222
|
-
winRatio: rules.metrics.round(winRatio, 4),
|
|
223
|
-
avgDailyReturn: rules.metrics.round(avgDailyReturn, 4),
|
|
224
|
-
returnVolatility: rules.metrics.round(returnVolatility, 4),
|
|
225
|
-
|
|
226
|
-
// Concentration
|
|
227
|
-
top3Concentration: rules.metrics.round(top3Concentration, 2),
|
|
228
|
-
|
|
229
|
-
// Overall grade
|
|
230
|
-
riskGrade,
|
|
231
|
-
|
|
232
|
-
// Metadata for debugging
|
|
233
|
-
daysAnalyzed: dailyReturns.length + 1
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Calculate a risk grade from A (best) to F (worst).
|
|
239
|
-
*/
|
|
240
|
-
_calculateRiskGrade(metrics, rules) {
|
|
241
|
-
let score = 50; // Start at middle
|
|
242
|
-
|
|
243
|
-
// Sharpe Ratio: > 2 is excellent, < 0.5 is poor
|
|
244
|
-
if (metrics.sharpeRatio > 2) score += 20;
|
|
245
|
-
else if (metrics.sharpeRatio > 1) score += 10;
|
|
246
|
-
else if (metrics.sharpeRatio < 0.5) score -= 10;
|
|
247
|
-
else if (metrics.sharpeRatio < 0) score -= 20;
|
|
248
|
-
|
|
249
|
-
// Max Drawdown: < 10% is good, > 30% is poor
|
|
250
|
-
if (metrics.maxDrawdown < 10) score += 15;
|
|
251
|
-
else if (metrics.maxDrawdown < 20) score += 5;
|
|
252
|
-
else if (metrics.maxDrawdown > 30) score -= 15;
|
|
253
|
-
else if (metrics.maxDrawdown > 50) score -= 25;
|
|
254
|
-
|
|
255
|
-
// Win Ratio: > 60% is good, < 40% is poor
|
|
256
|
-
if (metrics.winRatio > 0.6) score += 10;
|
|
257
|
-
else if (metrics.winRatio < 0.4) score -= 10;
|
|
258
|
-
|
|
259
|
-
// Concentration: > 50% in top 3 is risky
|
|
260
|
-
if (metrics.top3Concentration > 70) score -= 15;
|
|
261
|
-
else if (metrics.top3Concentration > 50) score -= 5;
|
|
262
|
-
else if (metrics.top3Concentration < 30) score += 5;
|
|
263
|
-
|
|
264
|
-
// Clamp and convert to grade
|
|
265
|
-
score = rules.metrics.clamp(score, 0, 100);
|
|
266
|
-
|
|
267
|
-
if (score >= 80) return 'A';
|
|
268
|
-
if (score >= 65) return 'B';
|
|
269
|
-
if (score >= 50) return 'C';
|
|
270
|
-
if (score >= 35) return 'D';
|
|
271
|
-
return 'F';
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
_emptyResult(entityId, date) {
|
|
275
|
-
return {
|
|
276
|
-
userId: entityId,
|
|
277
|
-
date,
|
|
278
|
-
portfolioValue: 0,
|
|
279
|
-
positionCount: 0,
|
|
280
|
-
sharpeRatio: 0,
|
|
281
|
-
sortinoRatio: 0,
|
|
282
|
-
maxDrawdown: 0,
|
|
283
|
-
valueAtRisk95: 0,
|
|
284
|
-
winRatio: 0,
|
|
285
|
-
avgDailyReturn: 0,
|
|
286
|
-
returnVolatility: 0,
|
|
287
|
-
top3Concentration: 0,
|
|
288
|
-
riskGrade: 'N/A',
|
|
289
|
-
daysAnalyzed: 0
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
module.exports = PopularInvestorRiskMetrics;
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview User Portfolio Summary - v2 Computation
|
|
3
|
-
*
|
|
4
|
-
* This computation demonstrates the "simple recipe" pattern:
|
|
5
|
-
* - No complex calculations inline
|
|
6
|
-
* - Uses injected rules for all business logic
|
|
7
|
-
* - The computation itself just orchestrates
|
|
8
|
-
*
|
|
9
|
-
* When rules.portfolio changes, this computation automatically re-runs
|
|
10
|
-
* because the rules hash is included in the computation hash.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
const { Computation } = require('../framework');
|
|
14
|
-
|
|
15
|
-
class UserPortfolioSummary extends Computation {
|
|
16
|
-
/**
|
|
17
|
-
* Configuration - What the framework needs to know.
|
|
18
|
-
*/
|
|
19
|
-
static getConfig() {
|
|
20
|
-
return {
|
|
21
|
-
name: 'UserPortfolioSummary',
|
|
22
|
-
category: 'analytics',
|
|
23
|
-
|
|
24
|
-
requires: {
|
|
25
|
-
'portfolio_snapshots': {
|
|
26
|
-
lookback: 0,
|
|
27
|
-
mandatory: true,
|
|
28
|
-
filter: { user_type: 'POPULAR_INVESTOR' }
|
|
29
|
-
},
|
|
30
|
-
'asset_prices': {
|
|
31
|
-
lookback: 0,
|
|
32
|
-
mandatory: true
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
|
|
36
|
-
dependencies: [],
|
|
37
|
-
type: 'per-entity',
|
|
38
|
-
ttlDays: 30,
|
|
39
|
-
|
|
40
|
-
// =========================================================================
|
|
41
|
-
// STORAGE CONFIGURATION
|
|
42
|
-
// =========================================================================
|
|
43
|
-
// Controls where computation results are persisted.
|
|
44
|
-
// BigQuery is always enabled unless explicitly set to false.
|
|
45
|
-
// Firestore is opt-in for frontend-facing, user-centric data.
|
|
46
|
-
// =========================================================================
|
|
47
|
-
storage: {
|
|
48
|
-
// BigQuery: always enabled by default (for analytics, dependency resolution)
|
|
49
|
-
bigquery: true,
|
|
50
|
-
|
|
51
|
-
// Firestore: user-centric storage for real-time frontend access
|
|
52
|
-
firestore: {
|
|
53
|
-
enabled: true,
|
|
54
|
-
// Path template - {entityId} replaced with user ID, {date} with computation date
|
|
55
|
-
// This stores each user's portfolio summary in their user document
|
|
56
|
-
path: '/users/{entityId}/computations/portfolio_summary',
|
|
57
|
-
|
|
58
|
-
// merge: false = overwrite entire document (default)
|
|
59
|
-
// merge: true = merge with existing fields
|
|
60
|
-
merge: false,
|
|
61
|
-
|
|
62
|
-
// Include metadata (_computedAt, _computationDate, _codeHash)
|
|
63
|
-
includeMetadata: true
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
static getSchema() {
|
|
70
|
-
return {
|
|
71
|
-
type: 'object',
|
|
72
|
-
properties: {
|
|
73
|
-
userId: { type: 'string' },
|
|
74
|
-
date: { type: 'string' },
|
|
75
|
-
totalValue: { type: 'number' },
|
|
76
|
-
totalInvested: { type: 'number' },
|
|
77
|
-
profitLoss: { type: 'number' },
|
|
78
|
-
profitLossPercent: { type: 'number' },
|
|
79
|
-
positionCount: { type: 'number' },
|
|
80
|
-
winRatio: { type: 'number' },
|
|
81
|
-
topPositions: { type: 'array' }
|
|
82
|
-
},
|
|
83
|
-
required: ['userId', 'date', 'totalValue']
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
static getWeight() {
|
|
88
|
-
return 1.5;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Process a single user's portfolio.
|
|
93
|
-
*
|
|
94
|
-
* NOTE: This is a "simple recipe" - it uses injected rules for all
|
|
95
|
-
* business logic. The computation just orchestrates the rules.
|
|
96
|
-
*
|
|
97
|
-
* Benefits:
|
|
98
|
-
* - Rules are the single source of truth
|
|
99
|
-
* - When rules change, this computation re-runs automatically
|
|
100
|
-
* - Easy to understand what calculations are being done
|
|
101
|
-
* - Easy to test rules in isolation
|
|
102
|
-
*/
|
|
103
|
-
async process(context) {
|
|
104
|
-
const { date, entityId, data, rules } = context;
|
|
105
|
-
|
|
106
|
-
const portfolio = data['portfolio_snapshots'];
|
|
107
|
-
const prices = data['asset_prices'];
|
|
108
|
-
|
|
109
|
-
// No data = no result
|
|
110
|
-
if (!portfolio) {
|
|
111
|
-
this.log('DEBUG', `No portfolio for user ${entityId}`);
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// =====================================================================
|
|
116
|
-
// USE INJECTED RULES - No inline business logic!
|
|
117
|
-
// =====================================================================
|
|
118
|
-
|
|
119
|
-
// Extract positions using portfolio rules
|
|
120
|
-
const positions = rules.portfolio.extractPositions(portfolio);
|
|
121
|
-
|
|
122
|
-
if (!positions || positions.length === 0) {
|
|
123
|
-
this.setResult(entityId, this._emptyResult(entityId, date));
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Calculate portfolio metrics using portfolio rules
|
|
128
|
-
const totalValue = rules.portfolio.calculateTotalValue(positions);
|
|
129
|
-
const totalInvested = rules.portfolio.calculateTotalInvested(positions);
|
|
130
|
-
const profitLossPercent = rules.portfolio.calculateWeightedProfitPercent(positions);
|
|
131
|
-
const winRatio = rules.portfolio.calculateWinRatio(positions);
|
|
132
|
-
const topPositions = rules.portfolio.getTopPositions(positions, 5);
|
|
133
|
-
|
|
134
|
-
// Use metrics rules for rounding
|
|
135
|
-
const profitLoss = rules.metrics.round(totalValue - totalInvested, 2);
|
|
136
|
-
|
|
137
|
-
// Build result
|
|
138
|
-
this.setResult(entityId, {
|
|
139
|
-
userId: entityId,
|
|
140
|
-
date,
|
|
141
|
-
totalValue: rules.metrics.round(totalValue, 2),
|
|
142
|
-
totalInvested: rules.metrics.round(totalInvested, 2),
|
|
143
|
-
profitLoss,
|
|
144
|
-
profitLossPercent: rules.metrics.round(profitLossPercent, 2),
|
|
145
|
-
positionCount: positions.length,
|
|
146
|
-
winRatio: rules.metrics.round(winRatio, 4),
|
|
147
|
-
topPositions: topPositions.map(pos => ({
|
|
148
|
-
instrumentId: rules.portfolio.getInstrumentId(pos),
|
|
149
|
-
value: rules.metrics.round(rules.portfolio.getValue(pos), 2),
|
|
150
|
-
percentOfPortfolio: totalValue > 0
|
|
151
|
-
? rules.metrics.round((rules.portfolio.getValue(pos) / totalValue) * 100, 2)
|
|
152
|
-
: 0
|
|
153
|
-
}))
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
_emptyResult(entityId, date) {
|
|
158
|
-
return {
|
|
159
|
-
userId: entityId,
|
|
160
|
-
date,
|
|
161
|
-
totalValue: 0,
|
|
162
|
-
totalInvested: 0,
|
|
163
|
-
profitLoss: 0,
|
|
164
|
-
profitLossPercent: 0,
|
|
165
|
-
positionCount: 0,
|
|
166
|
-
winRatio: 0,
|
|
167
|
-
topPositions: []
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
module.exports = UserPortfolioSummary;
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
const admin = require('firebase-admin');
|
|
2
|
-
const { BigQuery } = require('@google-cloud/bigquery');
|
|
3
|
-
|
|
4
|
-
// Configuration
|
|
5
|
-
const PROJECT_ID = process.env.GCP_PROJECT_ID || 'stocks-12345';
|
|
6
|
-
const DATASET_ID = 'bulltrackers_data'; // Check your config
|
|
7
|
-
const TABLE_ID = 'sector_mappings';
|
|
8
|
-
const FIRESTORE_PATH = 'instrument_sector_mappings/sector_mappings';
|
|
9
|
-
|
|
10
|
-
async function migrate() {
|
|
11
|
-
console.log('🚀 Starting Sector Migration...');
|
|
12
|
-
|
|
13
|
-
// 1. Initialize Firestore
|
|
14
|
-
// Note: Assuming Application Default Credentials work.
|
|
15
|
-
// If running locally, ensure you've done: gcloud auth application-default login
|
|
16
|
-
admin.initializeApp({ projectId: PROJECT_ID });
|
|
17
|
-
const db = admin.firestore();
|
|
18
|
-
|
|
19
|
-
// 2. Fetch Firestore Data
|
|
20
|
-
console.log(`Reading Firestore doc: ${FIRESTORE_PATH}...`);
|
|
21
|
-
const docRef = db.doc(FIRESTORE_PATH);
|
|
22
|
-
const doc = await docRef.get();
|
|
23
|
-
|
|
24
|
-
if (!doc.exists) {
|
|
25
|
-
throw new Error('Firestore document not found!');
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const data = doc.data();
|
|
29
|
-
const rows = Object.entries(data).map(([ticker, sector]) => ({
|
|
30
|
-
symbol: ticker,
|
|
31
|
-
sector: sector || 'N/A'
|
|
32
|
-
}));
|
|
33
|
-
|
|
34
|
-
console.log(`✅ Found ${rows.length} mappings.`);
|
|
35
|
-
|
|
36
|
-
// 3. Initialize BigQuery
|
|
37
|
-
const bigquery = new BigQuery({ projectId: PROJECT_ID });
|
|
38
|
-
const dataset = bigquery.dataset(DATASET_ID);
|
|
39
|
-
const table = dataset.table(TABLE_ID);
|
|
40
|
-
|
|
41
|
-
// 4. Create Table (if not exists)
|
|
42
|
-
const schema = [
|
|
43
|
-
{ name: 'symbol', type: 'STRING', mode: 'REQUIRED' },
|
|
44
|
-
{ name: 'sector', type: 'STRING', mode: 'NULLABLE' }
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
const [tableExists] = await table.exists();
|
|
48
|
-
|
|
49
|
-
if (!tableExists) {
|
|
50
|
-
console.log(`Creating table ${TABLE_ID}...`);
|
|
51
|
-
await table.create({ schema });
|
|
52
|
-
} else {
|
|
53
|
-
console.log(`Table ${TABLE_ID} exists. Truncating...`);
|
|
54
|
-
// We truncate to ensure a clean slate (idempotent migration)
|
|
55
|
-
await bigquery.query(`TRUNCATE TABLE \`${PROJECT_ID}.${DATASET_ID}.${TABLE_ID}\``);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// 5. Insert Data
|
|
59
|
-
if (rows.length > 0) {
|
|
60
|
-
console.log('Inserting rows into BigQuery...');
|
|
61
|
-
// Insert in chunks to be safe
|
|
62
|
-
const chunkSize = 1000;
|
|
63
|
-
for (let i = 0; i < rows.length; i += chunkSize) {
|
|
64
|
-
const chunk = rows.slice(i, i + chunkSize);
|
|
65
|
-
await table.insert(chunk);
|
|
66
|
-
console.log(` Inserted rows ${i + 1} to ${i + chunk.length}`);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
console.log('🎉 Migration complete!');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
migrate().catch(console.error);
|