bulltrackers-module 1.0.733 → 1.0.734
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/README.md +152 -0
- package/functions/computation-system-v2/computations/PopularInvestorProfileMetrics.js +720 -0
- package/functions/computation-system-v2/computations/PopularInvestorRiskAssessment.js +176 -0
- package/functions/computation-system-v2/computations/PopularInvestorRiskMetrics.js +294 -0
- package/functions/computation-system-v2/computations/TestComputation.js +46 -0
- package/functions/computation-system-v2/computations/UserPortfolioSummary.js +172 -0
- package/functions/computation-system-v2/config/bulltrackers.config.js +317 -0
- package/functions/computation-system-v2/framework/core/Computation.js +73 -0
- package/functions/computation-system-v2/framework/core/Manifest.js +223 -0
- package/functions/computation-system-v2/framework/core/RuleInjector.js +53 -0
- package/functions/computation-system-v2/framework/core/Rules.js +231 -0
- package/functions/computation-system-v2/framework/core/RunAnalyzer.js +163 -0
- package/functions/computation-system-v2/framework/cost/CostTracker.js +154 -0
- package/functions/computation-system-v2/framework/data/DataFetcher.js +399 -0
- package/functions/computation-system-v2/framework/data/QueryBuilder.js +232 -0
- package/functions/computation-system-v2/framework/data/SchemaRegistry.js +287 -0
- package/functions/computation-system-v2/framework/execution/Orchestrator.js +498 -0
- package/functions/computation-system-v2/framework/execution/TaskRunner.js +35 -0
- package/functions/computation-system-v2/framework/execution/middleware/CostTrackerMiddleware.js +32 -0
- package/functions/computation-system-v2/framework/execution/middleware/LineageMiddleware.js +32 -0
- package/functions/computation-system-v2/framework/execution/middleware/Middleware.js +14 -0
- package/functions/computation-system-v2/framework/execution/middleware/ProfilerMiddleware.js +47 -0
- package/functions/computation-system-v2/framework/index.js +45 -0
- package/functions/computation-system-v2/framework/lineage/LineageTracker.js +147 -0
- package/functions/computation-system-v2/framework/monitoring/Profiler.js +80 -0
- package/functions/computation-system-v2/framework/resilience/Checkpointer.js +66 -0
- package/functions/computation-system-v2/framework/scheduling/ScheduleValidator.js +327 -0
- package/functions/computation-system-v2/framework/storage/StateRepository.js +286 -0
- package/functions/computation-system-v2/framework/storage/StorageManager.js +469 -0
- package/functions/computation-system-v2/framework/storage/index.js +9 -0
- package/functions/computation-system-v2/framework/testing/ComputationTester.js +86 -0
- package/functions/computation-system-v2/framework/utils/Graph.js +205 -0
- package/functions/computation-system-v2/handlers/dispatcher.js +109 -0
- package/functions/computation-system-v2/handlers/index.js +23 -0
- package/functions/computation-system-v2/handlers/onDemand.js +289 -0
- package/functions/computation-system-v2/handlers/scheduler.js +327 -0
- package/functions/computation-system-v2/index.js +163 -0
- package/functions/computation-system-v2/rules/index.js +49 -0
- package/functions/computation-system-v2/rules/instruments.js +465 -0
- package/functions/computation-system-v2/rules/metrics.js +304 -0
- package/functions/computation-system-v2/rules/portfolio.js +534 -0
- package/functions/computation-system-v2/rules/rankings.js +655 -0
- package/functions/computation-system-v2/rules/social.js +562 -0
- package/functions/computation-system-v2/rules/trades.js +545 -0
- package/functions/computation-system-v2/scripts/migrate-sectors.js +73 -0
- package/functions/computation-system-v2/test/test-dispatcher.js +317 -0
- package/functions/computation-system-v2/test/test-framework.js +500 -0
- package/functions/computation-system-v2/test/test-real-execution.js +166 -0
- package/functions/computation-system-v2/test/test-real-integration.js +194 -0
- package/functions/computation-system-v2/test/test-refactor-e2e.js +131 -0
- package/functions/computation-system-v2/test/test-results.json +31 -0
- package/functions/computation-system-v2/test/test-risk-metrics-computation.js +329 -0
- package/functions/computation-system-v2/test/test-scheduler.js +204 -0
- package/functions/computation-system-v2/test/test-storage.js +449 -0
- package/functions/orchestrator/index.js +18 -26
- package/package.json +3 -2
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Portfolio Business Rules
|
|
3
|
+
*
|
|
4
|
+
* For extracting and analyzing portfolio_snapshots data.
|
|
5
|
+
*
|
|
6
|
+
* Portfolio data structure (portfolio_data JSON):
|
|
7
|
+
* - AggregatedPositions: Array of positions per instrument
|
|
8
|
+
* - AggregatedMirrors: Array of copied positions
|
|
9
|
+
* - AggregatedPositionsByInstrumentTypeID: Positions by asset class
|
|
10
|
+
* - CreditByRealizedEquity: % cash relative to invested sum
|
|
11
|
+
* - CreditByUnrealizedEquity: % cash relative to current balance
|
|
12
|
+
*
|
|
13
|
+
* Position data (percentages, not absolute values):
|
|
14
|
+
* - Direction: 'Buy' or 'Sell'
|
|
15
|
+
* - InstrumentID: Asset ID
|
|
16
|
+
* - Invested: % of initial investment
|
|
17
|
+
* - NetProfit: % P&L
|
|
18
|
+
* - Value: % of unrealized portfolio value
|
|
19
|
+
*
|
|
20
|
+
* Usage in computation:
|
|
21
|
+
* ```javascript
|
|
22
|
+
* const data = rules.portfolio.extractPortfolioData(row);
|
|
23
|
+
* const positions = rules.portfolio.extractPositions(data);
|
|
24
|
+
* const totalValue = rules.portfolio.calculateTotalValue(positions);
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// DATA EXTRACTION
|
|
30
|
+
// =============================================================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Extract portfolio_data from a portfolio_snapshots row.
|
|
34
|
+
* @param {Object} row - portfolio_snapshots row
|
|
35
|
+
* @returns {Object|null}
|
|
36
|
+
*/
|
|
37
|
+
function extractPortfolioData(row) {
|
|
38
|
+
if (!row) return null;
|
|
39
|
+
|
|
40
|
+
let data = row.portfolio_data;
|
|
41
|
+
|
|
42
|
+
if (typeof data === 'string') {
|
|
43
|
+
try {
|
|
44
|
+
data = JSON.parse(data);
|
|
45
|
+
} catch (e) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return data || null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get user ID from portfolio row.
|
|
55
|
+
* @param {Object} row - portfolio_snapshots row
|
|
56
|
+
* @returns {string|null}
|
|
57
|
+
*/
|
|
58
|
+
function getUserId(row) {
|
|
59
|
+
if (!row) return null;
|
|
60
|
+
|
|
61
|
+
if (row.user_id) return String(row.user_id);
|
|
62
|
+
|
|
63
|
+
const data = extractPortfolioData(row);
|
|
64
|
+
return data?.cid ? String(data.cid) : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get username from portfolio data.
|
|
69
|
+
* @param {Object} data - portfolio_data object
|
|
70
|
+
* @returns {string|null}
|
|
71
|
+
*/
|
|
72
|
+
function getUsername(data) {
|
|
73
|
+
return data?.username || null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Extract positions from portfolio data.
|
|
78
|
+
* Handles different portfolio data structures.
|
|
79
|
+
*
|
|
80
|
+
* @param {Object|Array} portfolioData - Raw portfolio data from BigQuery
|
|
81
|
+
* @returns {Array} Array of position objects
|
|
82
|
+
*/
|
|
83
|
+
function extractPositions(portfolioData) {
|
|
84
|
+
if (!portfolioData) return [];
|
|
85
|
+
|
|
86
|
+
// Handle array directly
|
|
87
|
+
if (Array.isArray(portfolioData)) return portfolioData;
|
|
88
|
+
|
|
89
|
+
// Handle nested structures (actual data structure)
|
|
90
|
+
if (portfolioData.AggregatedPositions) return portfolioData.AggregatedPositions;
|
|
91
|
+
if (portfolioData.Positions) return portfolioData.Positions;
|
|
92
|
+
if (portfolioData.positions) return portfolioData.positions;
|
|
93
|
+
|
|
94
|
+
// Handle JSON string
|
|
95
|
+
if (typeof portfolioData === 'string') {
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(portfolioData);
|
|
98
|
+
return extractPositions(parsed);
|
|
99
|
+
} catch (e) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Extract mirrored (copied) positions.
|
|
109
|
+
* @param {Object} portfolioData - portfolio_data object
|
|
110
|
+
* @returns {Array}
|
|
111
|
+
*/
|
|
112
|
+
function extractMirrors(portfolioData) {
|
|
113
|
+
if (!portfolioData) return [];
|
|
114
|
+
return portfolioData.AggregatedMirrors || [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Extract positions by instrument type/asset class.
|
|
119
|
+
* @param {Object} portfolioData - portfolio_data object
|
|
120
|
+
* @returns {Array}
|
|
121
|
+
*/
|
|
122
|
+
function extractPositionsByType(portfolioData) {
|
|
123
|
+
if (!portfolioData) return [];
|
|
124
|
+
return portfolioData.AggregatedPositionsByInstrumentTypeID || [];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Extract positions by stock industry.
|
|
129
|
+
* @param {Object} portfolioData - portfolio_data object
|
|
130
|
+
* @returns {Array}
|
|
131
|
+
*/
|
|
132
|
+
function extractPositionsByIndustry(portfolioData) {
|
|
133
|
+
if (!portfolioData) return [];
|
|
134
|
+
return portfolioData.AggregatedPositionsByStockIndustryID || [];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// =============================================================================
|
|
138
|
+
// CASH BALANCE
|
|
139
|
+
// =============================================================================
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get cash balance as % of realized equity (invested sum).
|
|
143
|
+
* @param {Object} portfolioData - portfolio_data object
|
|
144
|
+
* @returns {number}
|
|
145
|
+
*/
|
|
146
|
+
function getCreditByRealizedEquity(portfolioData) {
|
|
147
|
+
return portfolioData?.CreditByRealizedEquity ?? 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get cash balance as % of unrealized equity (current balance).
|
|
152
|
+
* @param {Object} portfolioData - portfolio_data object
|
|
153
|
+
* @returns {number}
|
|
154
|
+
*/
|
|
155
|
+
function getCreditByUnrealizedEquity(portfolioData) {
|
|
156
|
+
return portfolioData?.CreditByUnrealizedEquity ?? 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Calculate exposure percentage (100 - cash %).
|
|
161
|
+
* @param {Object} portfolioData - portfolio_data object
|
|
162
|
+
* @returns {number}
|
|
163
|
+
*/
|
|
164
|
+
function getExposure(portfolioData) {
|
|
165
|
+
return 100 - getCreditByUnrealizedEquity(portfolioData);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// =============================================================================
|
|
169
|
+
// POSITION ACCESSORS
|
|
170
|
+
// =============================================================================
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get the instrument ID from a position.
|
|
174
|
+
* Handles different field names.
|
|
175
|
+
*
|
|
176
|
+
* @param {Object} position - Position object
|
|
177
|
+
* @returns {string|number|null} Instrument ID
|
|
178
|
+
*/
|
|
179
|
+
function getInstrumentId(position) {
|
|
180
|
+
return position?.InstrumentID ||
|
|
181
|
+
position?.instrumentId ||
|
|
182
|
+
position?.instrument_id ||
|
|
183
|
+
null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get the instrument type ID from a position.
|
|
188
|
+
* @param {Object} position - Position object
|
|
189
|
+
* @returns {number|null}
|
|
190
|
+
*/
|
|
191
|
+
function getInstrumentTypeId(position) {
|
|
192
|
+
return position?.InstrumentTypeID || null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get the direction of a position ('Buy' or 'Sell').
|
|
197
|
+
* @param {Object} position - Position object
|
|
198
|
+
* @returns {string}
|
|
199
|
+
*/
|
|
200
|
+
function getDirection(position) {
|
|
201
|
+
return position?.Direction || 'Buy';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Check if position is long (Buy).
|
|
206
|
+
* @param {Object} position - Position object
|
|
207
|
+
* @returns {boolean}
|
|
208
|
+
*/
|
|
209
|
+
function isLong(position) {
|
|
210
|
+
return getDirection(position) === 'Buy';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Check if position is short (Sell).
|
|
215
|
+
* @param {Object} position - Position object
|
|
216
|
+
* @returns {boolean}
|
|
217
|
+
*/
|
|
218
|
+
function isShort(position) {
|
|
219
|
+
return getDirection(position) === 'Sell';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get the invested percentage (% of initial investment).
|
|
224
|
+
* NOTE: This is a percentage, not an absolute value.
|
|
225
|
+
*
|
|
226
|
+
* @param {Object} position - Position object
|
|
227
|
+
* @returns {number} Invested percentage
|
|
228
|
+
*/
|
|
229
|
+
function getInvested(position) {
|
|
230
|
+
return position?.Invested ||
|
|
231
|
+
position?.Amount ||
|
|
232
|
+
position?.amount ||
|
|
233
|
+
position?.invested ||
|
|
234
|
+
position?.Investment ||
|
|
235
|
+
0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get the net profit percentage (% P&L).
|
|
240
|
+
* NOTE: This is a percentage, not an absolute value.
|
|
241
|
+
*
|
|
242
|
+
* @param {Object} position - Position object
|
|
243
|
+
* @returns {number} Net profit percentage
|
|
244
|
+
*/
|
|
245
|
+
function getNetProfit(position) {
|
|
246
|
+
return position?.NetProfit ||
|
|
247
|
+
position?.netProfit ||
|
|
248
|
+
position?.net_profit ||
|
|
249
|
+
position?.Profit ||
|
|
250
|
+
0;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get the current value of a position.
|
|
255
|
+
*
|
|
256
|
+
* @param {Object} position - Position object
|
|
257
|
+
* @returns {number} Current value
|
|
258
|
+
*/
|
|
259
|
+
function getValue(position) {
|
|
260
|
+
const value = position?.Value || position?.value;
|
|
261
|
+
if (value) return value;
|
|
262
|
+
|
|
263
|
+
// Calculate from invested + profit
|
|
264
|
+
const invested = getInvested(position);
|
|
265
|
+
const profit = getNetProfit(position);
|
|
266
|
+
return invested + (invested * profit / 100);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Calculate total portfolio value.
|
|
271
|
+
*
|
|
272
|
+
* @param {Array} positions - Array of positions
|
|
273
|
+
* @returns {number} Total portfolio value
|
|
274
|
+
*/
|
|
275
|
+
function calculateTotalValue(positions) {
|
|
276
|
+
if (!Array.isArray(positions)) return 0;
|
|
277
|
+
return positions.reduce((sum, pos) => sum + getValue(pos), 0);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Calculate total invested amount.
|
|
282
|
+
*
|
|
283
|
+
* @param {Array} positions - Array of positions
|
|
284
|
+
* @returns {number} Total invested
|
|
285
|
+
*/
|
|
286
|
+
function calculateTotalInvested(positions) {
|
|
287
|
+
if (!Array.isArray(positions)) return 0;
|
|
288
|
+
return positions.reduce((sum, pos) => sum + getInvested(pos), 0);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Calculate weighted average profit percentage.
|
|
293
|
+
*
|
|
294
|
+
* @param {Array} positions - Array of positions
|
|
295
|
+
* @returns {number} Weighted average profit percentage
|
|
296
|
+
*/
|
|
297
|
+
function calculateWeightedProfitPercent(positions) {
|
|
298
|
+
if (!Array.isArray(positions) || positions.length === 0) return 0;
|
|
299
|
+
|
|
300
|
+
let totalWeightedProfit = 0;
|
|
301
|
+
let totalInvested = 0;
|
|
302
|
+
|
|
303
|
+
for (const pos of positions) {
|
|
304
|
+
const invested = getInvested(pos);
|
|
305
|
+
const profit = getNetProfit(pos);
|
|
306
|
+
|
|
307
|
+
if (invested > 0) {
|
|
308
|
+
totalWeightedProfit += invested * (profit / 100);
|
|
309
|
+
totalInvested += invested;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return totalInvested > 0
|
|
314
|
+
? (totalWeightedProfit / totalInvested) * 100
|
|
315
|
+
: 0;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Group positions by sector.
|
|
320
|
+
*
|
|
321
|
+
* @param {Array} positions - Array of positions
|
|
322
|
+
* @param {Object} sectorMap - Map of instrument ID -> sector name
|
|
323
|
+
* @returns {Object} Map of sector -> [positions]
|
|
324
|
+
*/
|
|
325
|
+
function groupBySector(positions, sectorMap) {
|
|
326
|
+
const groups = {};
|
|
327
|
+
|
|
328
|
+
for (const pos of (positions || [])) {
|
|
329
|
+
const instrumentId = getInstrumentId(pos);
|
|
330
|
+
const sector = sectorMap?.[String(instrumentId)] || 'Unknown';
|
|
331
|
+
|
|
332
|
+
if (!groups[sector]) groups[sector] = [];
|
|
333
|
+
groups[sector].push(pos);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return groups;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Calculate sector exposure percentages.
|
|
341
|
+
*
|
|
342
|
+
* @param {Array} positions - Array of positions
|
|
343
|
+
* @param {Object} sectorMap - Map of instrument ID -> sector name
|
|
344
|
+
* @returns {Object} Map of sector -> percentage
|
|
345
|
+
*/
|
|
346
|
+
function calculateSectorExposure(positions, sectorMap) {
|
|
347
|
+
const groups = groupBySector(positions, sectorMap);
|
|
348
|
+
const totalValue = calculateTotalValue(positions);
|
|
349
|
+
|
|
350
|
+
if (totalValue === 0) return {};
|
|
351
|
+
|
|
352
|
+
const exposure = {};
|
|
353
|
+
for (const [sector, sectorPositions] of Object.entries(groups)) {
|
|
354
|
+
const sectorValue = calculateTotalValue(sectorPositions);
|
|
355
|
+
exposure[sector] = Number(((sectorValue / totalValue) * 100).toFixed(2));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return exposure;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Get top N positions by value.
|
|
363
|
+
*
|
|
364
|
+
* @param {Array} positions - Array of positions
|
|
365
|
+
* @param {number} n - Number of top positions to return
|
|
366
|
+
* @returns {Array} Top N positions sorted by value descending
|
|
367
|
+
*/
|
|
368
|
+
function getTopPositions(positions, n = 10) {
|
|
369
|
+
if (!Array.isArray(positions)) return [];
|
|
370
|
+
|
|
371
|
+
return [...positions]
|
|
372
|
+
.sort((a, b) => getValue(b) - getValue(a))
|
|
373
|
+
.slice(0, n);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Get profitable positions.
|
|
378
|
+
*
|
|
379
|
+
* @param {Array} positions - Array of positions
|
|
380
|
+
* @returns {Array} Positions with positive net profit
|
|
381
|
+
*/
|
|
382
|
+
function getProfitablePositions(positions) {
|
|
383
|
+
if (!Array.isArray(positions)) return [];
|
|
384
|
+
return positions.filter(pos => getNetProfit(pos) > 0);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Calculate win ratio (% of profitable positions).
|
|
389
|
+
*
|
|
390
|
+
* @param {Array} positions - Array of positions
|
|
391
|
+
* @returns {number} Win ratio between 0 and 1
|
|
392
|
+
*/
|
|
393
|
+
function calculateWinRatio(positions) {
|
|
394
|
+
if (!Array.isArray(positions) || positions.length === 0) return 0;
|
|
395
|
+
|
|
396
|
+
const profitable = getProfitablePositions(positions);
|
|
397
|
+
return profitable.length / positions.length;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Filter long positions.
|
|
402
|
+
* @param {Array} positions - Array of positions
|
|
403
|
+
* @returns {Array}
|
|
404
|
+
*/
|
|
405
|
+
function filterLong(positions) {
|
|
406
|
+
if (!Array.isArray(positions)) return [];
|
|
407
|
+
return positions.filter(isLong);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Filter short positions.
|
|
412
|
+
* @param {Array} positions - Array of positions
|
|
413
|
+
* @returns {Array}
|
|
414
|
+
*/
|
|
415
|
+
function filterShort(positions) {
|
|
416
|
+
if (!Array.isArray(positions)) return [];
|
|
417
|
+
return positions.filter(isShort);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Calculate long/short exposure breakdown.
|
|
422
|
+
* @param {Array} positions - Array of positions
|
|
423
|
+
* @returns {Object} { longPct, shortPct, longCount, shortCount }
|
|
424
|
+
*/
|
|
425
|
+
function getLongShortBreakdown(positions) {
|
|
426
|
+
if (!Array.isArray(positions) || positions.length === 0) {
|
|
427
|
+
return { longPct: 0, shortPct: 0, longCount: 0, shortCount: 0 };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const long = filterLong(positions);
|
|
431
|
+
const short = filterShort(positions);
|
|
432
|
+
|
|
433
|
+
const longValue = calculateTotalValue(long);
|
|
434
|
+
const shortValue = calculateTotalValue(short);
|
|
435
|
+
const totalValue = longValue + shortValue;
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
longPct: totalValue > 0 ? (longValue / totalValue) * 100 : 0,
|
|
439
|
+
shortPct: totalValue > 0 ? (shortValue / totalValue) * 100 : 0,
|
|
440
|
+
longCount: long.length,
|
|
441
|
+
shortCount: short.length
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Summarize portfolio snapshot.
|
|
447
|
+
* @param {Object} row - portfolio_snapshots row
|
|
448
|
+
* @returns {Object}
|
|
449
|
+
*/
|
|
450
|
+
function summarize(row) {
|
|
451
|
+
const data = extractPortfolioData(row);
|
|
452
|
+
if (!data) return null;
|
|
453
|
+
|
|
454
|
+
const positions = extractPositions(data);
|
|
455
|
+
const longShort = getLongShortBreakdown(positions);
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
userId: getUserId(row),
|
|
459
|
+
username: getUsername(data),
|
|
460
|
+
|
|
461
|
+
// Positions
|
|
462
|
+
positionCount: positions.length,
|
|
463
|
+
totalValue: calculateTotalValue(positions),
|
|
464
|
+
totalInvested: calculateTotalInvested(positions),
|
|
465
|
+
|
|
466
|
+
// Performance
|
|
467
|
+
profitPercent: calculateWeightedProfitPercent(positions),
|
|
468
|
+
winRatio: calculateWinRatio(positions),
|
|
469
|
+
|
|
470
|
+
// Exposure
|
|
471
|
+
exposure: getExposure(data),
|
|
472
|
+
cashPercent: getCreditByUnrealizedEquity(data),
|
|
473
|
+
|
|
474
|
+
// Direction
|
|
475
|
+
longPct: longShort.longPct,
|
|
476
|
+
shortPct: longShort.shortPct,
|
|
477
|
+
|
|
478
|
+
// Top holdings
|
|
479
|
+
topPositions: getTopPositions(positions, 5).map(pos => ({
|
|
480
|
+
instrumentId: getInstrumentId(pos),
|
|
481
|
+
value: getValue(pos),
|
|
482
|
+
profit: getNetProfit(pos),
|
|
483
|
+
direction: getDirection(pos)
|
|
484
|
+
}))
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
module.exports = {
|
|
489
|
+
// Data extraction
|
|
490
|
+
extractPortfolioData,
|
|
491
|
+
getUserId,
|
|
492
|
+
getUsername,
|
|
493
|
+
extractPositions,
|
|
494
|
+
extractMirrors,
|
|
495
|
+
extractPositionsByType,
|
|
496
|
+
extractPositionsByIndustry,
|
|
497
|
+
|
|
498
|
+
// Cash balance
|
|
499
|
+
getCreditByRealizedEquity,
|
|
500
|
+
getCreditByUnrealizedEquity,
|
|
501
|
+
getExposure,
|
|
502
|
+
|
|
503
|
+
// Position accessors
|
|
504
|
+
getInstrumentId,
|
|
505
|
+
getInstrumentTypeId,
|
|
506
|
+
getDirection,
|
|
507
|
+
isLong,
|
|
508
|
+
isShort,
|
|
509
|
+
getInvested,
|
|
510
|
+
getNetProfit,
|
|
511
|
+
getValue,
|
|
512
|
+
|
|
513
|
+
// Calculations
|
|
514
|
+
calculateTotalValue,
|
|
515
|
+
calculateTotalInvested,
|
|
516
|
+
calculateWeightedProfitPercent,
|
|
517
|
+
calculateWinRatio,
|
|
518
|
+
|
|
519
|
+
// Grouping
|
|
520
|
+
groupBySector,
|
|
521
|
+
calculateSectorExposure,
|
|
522
|
+
|
|
523
|
+
// Filtering
|
|
524
|
+
filterLong,
|
|
525
|
+
filterShort,
|
|
526
|
+
getLongShortBreakdown,
|
|
527
|
+
|
|
528
|
+
// Ranking
|
|
529
|
+
getTopPositions,
|
|
530
|
+
getProfitablePositions,
|
|
531
|
+
|
|
532
|
+
// Summary
|
|
533
|
+
summarize
|
|
534
|
+
};
|