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,545 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Trade History Business Rules
|
|
3
|
+
*
|
|
4
|
+
* For extracting and analyzing trade_history_snapshots data.
|
|
5
|
+
* Each row contains PublicHistoryPositions - an array of closed trades.
|
|
6
|
+
*
|
|
7
|
+
* NOTE: Trade history shows individual closed positions. We can see:
|
|
8
|
+
* - Net profit percentage per trade
|
|
9
|
+
* - Open/close rates and dates
|
|
10
|
+
* - Leverage and direction
|
|
11
|
+
*
|
|
12
|
+
* We CANNOT determine the invested amount per position from this data alone.
|
|
13
|
+
*
|
|
14
|
+
* Usage in computation:
|
|
15
|
+
* ```javascript
|
|
16
|
+
* const trades = rules.trades.extractTrades(row);
|
|
17
|
+
* const stats = rules.trades.calculateTradeStats(trades);
|
|
18
|
+
* const byInstrument = rules.trades.groupByInstrument(trades);
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// DATA EXTRACTION
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Extract trades array from a trade_history_snapshots row.
|
|
28
|
+
* @param {Object} row - trade_history_snapshots row
|
|
29
|
+
* @returns {Array} Array of trade objects
|
|
30
|
+
*/
|
|
31
|
+
function extractTrades(row) {
|
|
32
|
+
if (!row) return [];
|
|
33
|
+
|
|
34
|
+
let data = row.history_data;
|
|
35
|
+
|
|
36
|
+
if (typeof data === 'string') {
|
|
37
|
+
try {
|
|
38
|
+
data = JSON.parse(data);
|
|
39
|
+
} catch (e) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return data?.PublicHistoryPositions || [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get user ID from trade history row.
|
|
49
|
+
* @param {Object} row - trade_history_snapshots row
|
|
50
|
+
* @returns {string|null}
|
|
51
|
+
*/
|
|
52
|
+
function getUserId(row) {
|
|
53
|
+
if (!row) return null;
|
|
54
|
+
|
|
55
|
+
// Try row-level first
|
|
56
|
+
if (row.user_id) return String(row.user_id);
|
|
57
|
+
|
|
58
|
+
// Try from history_data
|
|
59
|
+
let data = row.history_data;
|
|
60
|
+
if (typeof data === 'string') {
|
|
61
|
+
try {
|
|
62
|
+
data = JSON.parse(data);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return data?.cid ? String(data.cid) : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// =============================================================================
|
|
72
|
+
// TRADE PROPERTY ACCESSORS
|
|
73
|
+
// =============================================================================
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get instrument ID from a trade.
|
|
77
|
+
* @param {Object} trade - Trade object
|
|
78
|
+
* @returns {number|null}
|
|
79
|
+
*/
|
|
80
|
+
function getInstrumentId(trade) {
|
|
81
|
+
return trade?.InstrumentID || null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get net profit percentage.
|
|
86
|
+
* @param {Object} trade - Trade object
|
|
87
|
+
* @returns {number}
|
|
88
|
+
*/
|
|
89
|
+
function getNetProfit(trade) {
|
|
90
|
+
return trade?.NetProfit ?? 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get open rate (price at open).
|
|
95
|
+
* @param {Object} trade - Trade object
|
|
96
|
+
* @returns {number}
|
|
97
|
+
*/
|
|
98
|
+
function getOpenRate(trade) {
|
|
99
|
+
return trade?.OpenRate ?? 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get close rate (price at close).
|
|
104
|
+
* @param {Object} trade - Trade object
|
|
105
|
+
* @returns {number}
|
|
106
|
+
*/
|
|
107
|
+
function getCloseRate(trade) {
|
|
108
|
+
return trade?.CloseRate ?? 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get open date.
|
|
113
|
+
* @param {Object} trade - Trade object
|
|
114
|
+
* @returns {Date|null}
|
|
115
|
+
*/
|
|
116
|
+
function getOpenDate(trade) {
|
|
117
|
+
return trade?.OpenDateTime ? new Date(trade.OpenDateTime) : null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get close date.
|
|
122
|
+
* @param {Object} trade - Trade object
|
|
123
|
+
* @returns {Date|null}
|
|
124
|
+
*/
|
|
125
|
+
function getCloseDate(trade) {
|
|
126
|
+
return trade?.CloseDateTime ? new Date(trade.CloseDateTime) : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get leverage (integer).
|
|
131
|
+
* @param {Object} trade - Trade object
|
|
132
|
+
* @returns {number}
|
|
133
|
+
*/
|
|
134
|
+
function getLeverage(trade) {
|
|
135
|
+
return trade?.Leverage ?? 1;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Check if trade was a buy (long).
|
|
140
|
+
* @param {Object} trade - Trade object
|
|
141
|
+
* @returns {boolean}
|
|
142
|
+
*/
|
|
143
|
+
function isBuy(trade) {
|
|
144
|
+
return trade?.IsBuy === true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Check if trade was a sell (short).
|
|
149
|
+
* @param {Object} trade - Trade object
|
|
150
|
+
* @returns {boolean}
|
|
151
|
+
*/
|
|
152
|
+
function isSell(trade) {
|
|
153
|
+
return trade?.IsBuy === false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get position ID.
|
|
158
|
+
* @param {Object} trade - Trade object
|
|
159
|
+
* @returns {number|null}
|
|
160
|
+
*/
|
|
161
|
+
function getPositionId(trade) {
|
|
162
|
+
return trade?.PositionID || null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get close reason code.
|
|
167
|
+
* Close reasons: 0 = manual, 5 = stop loss, etc.
|
|
168
|
+
* @param {Object} trade - Trade object
|
|
169
|
+
* @returns {number}
|
|
170
|
+
*/
|
|
171
|
+
function getCloseReason(trade) {
|
|
172
|
+
return trade?.CloseReason ?? 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Check if trade was profitable.
|
|
177
|
+
* @param {Object} trade - Trade object
|
|
178
|
+
* @returns {boolean}
|
|
179
|
+
*/
|
|
180
|
+
function isProfitable(trade) {
|
|
181
|
+
return getNetProfit(trade) > 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Calculate trade duration in days.
|
|
186
|
+
* @param {Object} trade - Trade object
|
|
187
|
+
* @returns {number}
|
|
188
|
+
*/
|
|
189
|
+
function getDurationDays(trade) {
|
|
190
|
+
const open = getOpenDate(trade);
|
|
191
|
+
const close = getCloseDate(trade);
|
|
192
|
+
|
|
193
|
+
if (!open || !close) return 0;
|
|
194
|
+
|
|
195
|
+
return (close - open) / (1000 * 60 * 60 * 24);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Calculate price change percentage.
|
|
200
|
+
* @param {Object} trade - Trade object
|
|
201
|
+
* @returns {number}
|
|
202
|
+
*/
|
|
203
|
+
function getPriceChangePct(trade) {
|
|
204
|
+
const open = getOpenRate(trade);
|
|
205
|
+
const close = getCloseRate(trade);
|
|
206
|
+
|
|
207
|
+
if (!open || open === 0) return 0;
|
|
208
|
+
|
|
209
|
+
return ((close - open) / open) * 100;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// =============================================================================
|
|
213
|
+
// FILTERING
|
|
214
|
+
// =============================================================================
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Filter trades by date range.
|
|
218
|
+
* @param {Array} trades - Array of trades
|
|
219
|
+
* @param {Date|string} startDate - Start date
|
|
220
|
+
* @param {Date|string} endDate - End date
|
|
221
|
+
* @returns {Array}
|
|
222
|
+
*/
|
|
223
|
+
function filterByDateRange(trades, startDate, endDate) {
|
|
224
|
+
const start = new Date(startDate);
|
|
225
|
+
const end = new Date(endDate);
|
|
226
|
+
|
|
227
|
+
return trades.filter(trade => {
|
|
228
|
+
const closeDate = getCloseDate(trade);
|
|
229
|
+
return closeDate && closeDate >= start && closeDate <= end;
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Filter trades by instrument.
|
|
235
|
+
* @param {Array} trades - Array of trades
|
|
236
|
+
* @param {number} instrumentId - Instrument ID
|
|
237
|
+
* @returns {Array}
|
|
238
|
+
*/
|
|
239
|
+
function filterByInstrument(trades, instrumentId) {
|
|
240
|
+
return trades.filter(trade => getInstrumentId(trade) === instrumentId);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Filter to only profitable trades.
|
|
245
|
+
* @param {Array} trades - Array of trades
|
|
246
|
+
* @returns {Array}
|
|
247
|
+
*/
|
|
248
|
+
function filterProfitable(trades) {
|
|
249
|
+
return trades.filter(isProfitable);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Filter to only losing trades.
|
|
254
|
+
* @param {Array} trades - Array of trades
|
|
255
|
+
* @returns {Array}
|
|
256
|
+
*/
|
|
257
|
+
function filterLosing(trades) {
|
|
258
|
+
return trades.filter(trade => !isProfitable(trade));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Filter to only long trades.
|
|
263
|
+
* @param {Array} trades - Array of trades
|
|
264
|
+
* @returns {Array}
|
|
265
|
+
*/
|
|
266
|
+
function filterLong(trades) {
|
|
267
|
+
return trades.filter(isBuy);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Filter to only short trades.
|
|
272
|
+
* @param {Array} trades - Array of trades
|
|
273
|
+
* @returns {Array}
|
|
274
|
+
*/
|
|
275
|
+
function filterShort(trades) {
|
|
276
|
+
return trades.filter(isSell);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Filter by leverage.
|
|
281
|
+
* @param {Array} trades - Array of trades
|
|
282
|
+
* @param {number} minLeverage - Minimum leverage
|
|
283
|
+
* @param {number} [maxLeverage] - Maximum leverage
|
|
284
|
+
* @returns {Array}
|
|
285
|
+
*/
|
|
286
|
+
function filterByLeverage(trades, minLeverage, maxLeverage = Infinity) {
|
|
287
|
+
return trades.filter(trade => {
|
|
288
|
+
const lev = getLeverage(trade);
|
|
289
|
+
return lev >= minLeverage && lev <= maxLeverage;
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// =============================================================================
|
|
294
|
+
// GROUPING
|
|
295
|
+
// =============================================================================
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Group trades by instrument.
|
|
299
|
+
* @param {Array} trades - Array of trades
|
|
300
|
+
* @returns {Object} Map of instrumentId -> trades[]
|
|
301
|
+
*/
|
|
302
|
+
function groupByInstrument(trades) {
|
|
303
|
+
const groups = {};
|
|
304
|
+
|
|
305
|
+
for (const trade of trades) {
|
|
306
|
+
const id = getInstrumentId(trade) || 'unknown';
|
|
307
|
+
if (!groups[id]) groups[id] = [];
|
|
308
|
+
groups[id].push(trade);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return groups;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Group trades by month (YYYY-MM).
|
|
316
|
+
* @param {Array} trades - Array of trades
|
|
317
|
+
* @returns {Object} Map of month -> trades[]
|
|
318
|
+
*/
|
|
319
|
+
function groupByMonth(trades) {
|
|
320
|
+
const groups = {};
|
|
321
|
+
|
|
322
|
+
for (const trade of trades) {
|
|
323
|
+
const closeDate = getCloseDate(trade);
|
|
324
|
+
if (!closeDate) continue;
|
|
325
|
+
|
|
326
|
+
const month = `${closeDate.getFullYear()}-${String(closeDate.getMonth() + 1).padStart(2, '0')}`;
|
|
327
|
+
if (!groups[month]) groups[month] = [];
|
|
328
|
+
groups[month].push(trade);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return groups;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Group trades by close reason.
|
|
336
|
+
* @param {Array} trades - Array of trades
|
|
337
|
+
* @returns {Object} Map of closeReason -> trades[]
|
|
338
|
+
*/
|
|
339
|
+
function groupByCloseReason(trades) {
|
|
340
|
+
const groups = {};
|
|
341
|
+
|
|
342
|
+
for (const trade of trades) {
|
|
343
|
+
const reason = getCloseReason(trade);
|
|
344
|
+
if (!groups[reason]) groups[reason] = [];
|
|
345
|
+
groups[reason].push(trade);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return groups;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// =============================================================================
|
|
352
|
+
// STATISTICS
|
|
353
|
+
// =============================================================================
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Calculate comprehensive trade statistics.
|
|
357
|
+
* @param {Array} trades - Array of trades
|
|
358
|
+
* @returns {Object}
|
|
359
|
+
*/
|
|
360
|
+
function calculateTradeStats(trades) {
|
|
361
|
+
if (!trades || trades.length === 0) {
|
|
362
|
+
return {
|
|
363
|
+
totalTrades: 0,
|
|
364
|
+
profitableTrades: 0,
|
|
365
|
+
losingTrades: 0,
|
|
366
|
+
winRate: 0,
|
|
367
|
+
avgProfit: 0,
|
|
368
|
+
avgWin: 0,
|
|
369
|
+
avgLoss: 0,
|
|
370
|
+
maxWin: 0,
|
|
371
|
+
maxLoss: 0,
|
|
372
|
+
profitFactor: 0,
|
|
373
|
+
avgDuration: 0,
|
|
374
|
+
longTrades: 0,
|
|
375
|
+
shortTrades: 0
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const profitable = filterProfitable(trades);
|
|
380
|
+
const losing = filterLosing(trades);
|
|
381
|
+
const long = filterLong(trades);
|
|
382
|
+
const short = filterShort(trades);
|
|
383
|
+
|
|
384
|
+
const profits = trades.map(getNetProfit);
|
|
385
|
+
const wins = profitable.map(getNetProfit);
|
|
386
|
+
const losses = losing.map(getNetProfit);
|
|
387
|
+
const durations = trades.map(getDurationDays).filter(d => d > 0);
|
|
388
|
+
|
|
389
|
+
const totalProfit = profits.reduce((sum, p) => sum + p, 0);
|
|
390
|
+
const totalWins = wins.reduce((sum, p) => sum + p, 0);
|
|
391
|
+
const totalLosses = Math.abs(losses.reduce((sum, p) => sum + p, 0));
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
totalTrades: trades.length,
|
|
395
|
+
profitableTrades: profitable.length,
|
|
396
|
+
losingTrades: losing.length,
|
|
397
|
+
winRate: trades.length > 0 ? (profitable.length / trades.length) * 100 : 0,
|
|
398
|
+
|
|
399
|
+
avgProfit: trades.length > 0 ? totalProfit / trades.length : 0,
|
|
400
|
+
avgWin: wins.length > 0 ? totalWins / wins.length : 0,
|
|
401
|
+
avgLoss: losses.length > 0 ? totalLosses / losses.length : 0,
|
|
402
|
+
|
|
403
|
+
maxWin: wins.length > 0 ? Math.max(...wins) : 0,
|
|
404
|
+
maxLoss: losses.length > 0 ? Math.min(...losses) : 0,
|
|
405
|
+
|
|
406
|
+
profitFactor: totalLosses > 0 ? totalWins / totalLosses : totalWins > 0 ? Infinity : 0,
|
|
407
|
+
|
|
408
|
+
avgDuration: durations.length > 0 ? durations.reduce((sum, d) => sum + d, 0) / durations.length : 0,
|
|
409
|
+
|
|
410
|
+
longTrades: long.length,
|
|
411
|
+
shortTrades: short.length
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Calculate statistics per instrument.
|
|
417
|
+
* @param {Array} trades - Array of trades
|
|
418
|
+
* @returns {Object} Map of instrumentId -> stats
|
|
419
|
+
*/
|
|
420
|
+
function calculateStatsByInstrument(trades) {
|
|
421
|
+
const groups = groupByInstrument(trades);
|
|
422
|
+
const stats = {};
|
|
423
|
+
|
|
424
|
+
for (const [instrumentId, instrumentTrades] of Object.entries(groups)) {
|
|
425
|
+
stats[instrumentId] = {
|
|
426
|
+
...calculateTradeStats(instrumentTrades),
|
|
427
|
+
instrumentId
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return stats;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Get recent trades.
|
|
436
|
+
* @param {Array} trades - Array of trades
|
|
437
|
+
* @param {number} count - Number of trades to return
|
|
438
|
+
* @returns {Array}
|
|
439
|
+
*/
|
|
440
|
+
function getRecentTrades(trades, count = 10) {
|
|
441
|
+
return [...trades]
|
|
442
|
+
.sort((a, b) => {
|
|
443
|
+
const dateA = getCloseDate(a);
|
|
444
|
+
const dateB = getCloseDate(b);
|
|
445
|
+
return (dateB || 0) - (dateA || 0);
|
|
446
|
+
})
|
|
447
|
+
.slice(0, count);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Get best trades by profit.
|
|
452
|
+
* @param {Array} trades - Array of trades
|
|
453
|
+
* @param {number} count - Number of trades to return
|
|
454
|
+
* @returns {Array}
|
|
455
|
+
*/
|
|
456
|
+
function getBestTrades(trades, count = 10) {
|
|
457
|
+
return [...trades]
|
|
458
|
+
.sort((a, b) => getNetProfit(b) - getNetProfit(a))
|
|
459
|
+
.slice(0, count);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Get worst trades by profit.
|
|
464
|
+
* @param {Array} trades - Array of trades
|
|
465
|
+
* @param {number} count - Number of trades to return
|
|
466
|
+
* @returns {Array}
|
|
467
|
+
*/
|
|
468
|
+
function getWorstTrades(trades, count = 10) {
|
|
469
|
+
return [...trades]
|
|
470
|
+
.sort((a, b) => getNetProfit(a) - getNetProfit(b))
|
|
471
|
+
.slice(0, count);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Summarize trade activity.
|
|
476
|
+
* @param {Object} row - trade_history_snapshots row
|
|
477
|
+
* @returns {Object}
|
|
478
|
+
*/
|
|
479
|
+
function summarize(row) {
|
|
480
|
+
const trades = extractTrades(row);
|
|
481
|
+
const stats = calculateTradeStats(trades);
|
|
482
|
+
const byInstrument = calculateStatsByInstrument(trades);
|
|
483
|
+
|
|
484
|
+
// Find most traded instrument
|
|
485
|
+
let mostTraded = null;
|
|
486
|
+
let mostTradedCount = 0;
|
|
487
|
+
for (const [id, s] of Object.entries(byInstrument)) {
|
|
488
|
+
if (s.totalTrades > mostTradedCount) {
|
|
489
|
+
mostTraded = id;
|
|
490
|
+
mostTradedCount = s.totalTrades;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return {
|
|
495
|
+
userId: getUserId(row),
|
|
496
|
+
...stats,
|
|
497
|
+
uniqueInstruments: Object.keys(byInstrument).length,
|
|
498
|
+
mostTradedInstrument: mostTraded,
|
|
499
|
+
mostTradedCount
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
module.exports = {
|
|
504
|
+
// Data extraction
|
|
505
|
+
extractTrades,
|
|
506
|
+
getUserId,
|
|
507
|
+
|
|
508
|
+
// Trade accessors
|
|
509
|
+
getInstrumentId,
|
|
510
|
+
getNetProfit,
|
|
511
|
+
getOpenRate,
|
|
512
|
+
getCloseRate,
|
|
513
|
+
getOpenDate,
|
|
514
|
+
getCloseDate,
|
|
515
|
+
getLeverage,
|
|
516
|
+
isBuy,
|
|
517
|
+
isSell,
|
|
518
|
+
getPositionId,
|
|
519
|
+
getCloseReason,
|
|
520
|
+
isProfitable,
|
|
521
|
+
getDurationDays,
|
|
522
|
+
getPriceChangePct,
|
|
523
|
+
|
|
524
|
+
// Filtering
|
|
525
|
+
filterByDateRange,
|
|
526
|
+
filterByInstrument,
|
|
527
|
+
filterProfitable,
|
|
528
|
+
filterLosing,
|
|
529
|
+
filterLong,
|
|
530
|
+
filterShort,
|
|
531
|
+
filterByLeverage,
|
|
532
|
+
|
|
533
|
+
// Grouping
|
|
534
|
+
groupByInstrument,
|
|
535
|
+
groupByMonth,
|
|
536
|
+
groupByCloseReason,
|
|
537
|
+
|
|
538
|
+
// Statistics
|
|
539
|
+
calculateTradeStats,
|
|
540
|
+
calculateStatsByInstrument,
|
|
541
|
+
getRecentTrades,
|
|
542
|
+
getBestTrades,
|
|
543
|
+
getWorstTrades,
|
|
544
|
+
summarize
|
|
545
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
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);
|