bulltrackers-module 1.0.214 → 1.0.216
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/controllers/computation_controller.js +25 -82
- package/functions/computation-system/helpers/computation_dispatcher.js +11 -24
- package/functions/computation-system/helpers/computation_manifest_builder.js +19 -40
- package/functions/computation-system/helpers/computation_pass_runner.js +23 -63
- package/functions/computation-system/helpers/computation_worker.js +11 -53
- package/functions/computation-system/helpers/orchestration_helpers.js +227 -370
- package/functions/computation-system/utils/utils.js +20 -106
- package/functions/task-engine/helpers/update_helpers.js +34 -70
- package/package.json +1 -1
- package/functions/computation-system/layers/math_primitives.js +0 -744
|
@@ -1,744 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Math Layer - Single Source of Truth (V3 Final)
|
|
3
|
-
* Encapsulates Schema Knowledge, Mathematical Primitives, and Signal Extractors.
|
|
4
|
-
* * STRICT COMPLIANCE:
|
|
5
|
-
* - Adheres to 'schema.md' definitions.
|
|
6
|
-
* - standardizes access to P&L, Weights, and Rates.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const SCHEMAS = {
|
|
10
|
-
USER_TYPES: { NORMAL: 'normal', SPECULATOR: 'speculator' },
|
|
11
|
-
STYLES: { DAY_TRADER: 'Day Trader', SWING_TRADER: 'Swing Trader', INVESTOR: 'Investor' },
|
|
12
|
-
LABELS: { SMART: 'Smart Money', DUMB: 'Dumb Money', NEUTRAL: 'Neutral' }
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
class InsightsExtractor {
|
|
16
|
-
/**
|
|
17
|
-
* Extracts the raw array of insight objects from the context.
|
|
18
|
-
* Checks for standard context injection paths.
|
|
19
|
-
*/
|
|
20
|
-
static getInsights(context) {
|
|
21
|
-
// Support multiple potential injection paths depending on controller version
|
|
22
|
-
return context.insights || context.daily_instrument_insights || [];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* returns the specific insight object for a given instrument ID.
|
|
27
|
-
*/
|
|
28
|
-
static getInsightForInstrument(insights, instrumentId) {
|
|
29
|
-
if (!insights || !Array.isArray(insights)) return null;
|
|
30
|
-
return insights.find(i => i.instrumentId === instrumentId) || null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// --- Standard Metrics ---
|
|
34
|
-
|
|
35
|
-
static getTotalOwners(insight) {
|
|
36
|
-
return insight ? (insight.total || 0) : 0;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
static getLongPercent(insight) {
|
|
40
|
-
return insight ? (insight.buy || 0) : 0;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
static getShortPercent(insight) {
|
|
44
|
-
return insight ? (insight.sell || 0) : 0;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
static getGrowthPercent(insight) {
|
|
48
|
-
return insight ? (insight.growth || 0) : 0;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// --- Derived Counts (Estimated) ---
|
|
52
|
-
|
|
53
|
-
static getLongCount(insight) {
|
|
54
|
-
const total = this.getTotalOwners(insight);
|
|
55
|
-
const buyPct = this.getLongPercent(insight);
|
|
56
|
-
return Math.floor(total * (buyPct / 100));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
static getShortCount(insight) {
|
|
60
|
-
const total = this.getTotalOwners(insight);
|
|
61
|
-
const sellPct = this.getShortPercent(insight);
|
|
62
|
-
return Math.floor(total * (sellPct / 100));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Calculates the net change in users from yesterday based on growth %.
|
|
67
|
-
* Formula: NetChange = Total - (Total / (1 + Growth/100))
|
|
68
|
-
*/
|
|
69
|
-
static getNetOwnershipChange(insight) {
|
|
70
|
-
const total = this.getTotalOwners(insight);
|
|
71
|
-
const growth = this.getGrowthPercent(insight);
|
|
72
|
-
if (total === 0) return 0;
|
|
73
|
-
|
|
74
|
-
// Reverse engineer yesterday's count
|
|
75
|
-
// Today = Yesterday * (1 + growth)
|
|
76
|
-
// Yesterday = Today / (1 + growth)
|
|
77
|
-
const prevTotal = total / (1 + (growth / 100)); // TODO: Check precision issues
|
|
78
|
-
return Math.round(total - prevTotal);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
class UserClassifier {
|
|
83
|
-
/**
|
|
84
|
-
* Classifies a user as Smart/Dumb based on Win Rate and Profitability.
|
|
85
|
-
* @param {Object} historySummary - Result from HistoryExtractor.getSummary()
|
|
86
|
-
*/
|
|
87
|
-
static classifySmartDumb(historySummary) {
|
|
88
|
-
if (!historySummary || historySummary.totalTrades < 5) return SCHEMAS.LABELS.NEUTRAL; // Insufficient data
|
|
89
|
-
|
|
90
|
-
const { winRatio, avgProfitPct, avgLossPct } = historySummary;
|
|
91
|
-
|
|
92
|
-
// 1. The "Consistent Winner" (Smart)
|
|
93
|
-
if (winRatio > 60 && avgProfitPct > Math.abs(avgLossPct)) return SCHEMAS.LABELS.SMART;
|
|
94
|
-
|
|
95
|
-
// 2. The "Sniper" (Smart - Low Win Rate but huge winners)
|
|
96
|
-
if (winRatio > 30 && avgProfitPct > (Math.abs(avgLossPct) * 2.5)) return SCHEMAS.LABELS.SMART;
|
|
97
|
-
|
|
98
|
-
// 3. The "Bagholder" (Dumb - High win rate but one loss wipes them out)
|
|
99
|
-
if (winRatio > 80 && (Math.abs(avgLossPct) > avgProfitPct * 4)) return SCHEMAS.LABELS.DUMB;
|
|
100
|
-
|
|
101
|
-
// 4. The "Gambler" (Dumb - Losing money consistently)
|
|
102
|
-
if (winRatio < 40 && avgProfitPct < Math.abs(avgLossPct)) return SCHEMAS.LABELS.DUMB;
|
|
103
|
-
|
|
104
|
-
return SCHEMAS.LABELS.NEUTRAL;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Classifies trading style based on average holding time.
|
|
109
|
-
* @param {number} avgHoldingMinutes
|
|
110
|
-
*/
|
|
111
|
-
static classifyStyle(avgHoldingMinutes) {
|
|
112
|
-
if (avgHoldingMinutes <= 0) return SCHEMAS.STYLES.INVESTOR; // Default
|
|
113
|
-
|
|
114
|
-
if (avgHoldingMinutes < 60 * 24) return SCHEMAS.STYLES.DAY_TRADER; // < 1 Day
|
|
115
|
-
if (avgHoldingMinutes < 60 * 24 * 30) return SCHEMAS.STYLES.SWING_TRADER; // < 1 Month
|
|
116
|
-
return SCHEMAS.STYLES.INVESTOR; // > 1 Month
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
class DataExtractor { // For generic access of data types
|
|
121
|
-
// ========================================================================
|
|
122
|
-
// 1. COLLECTION ACCESSORS
|
|
123
|
-
// ========================================================================
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Extract positions array based on User Type.
|
|
127
|
-
* - Normal: Uses 'AggregatedPositions' (Grouped by Asset + Direction)
|
|
128
|
-
* - Speculator: Uses 'PublicPositions' (Individual Trades)
|
|
129
|
-
*/
|
|
130
|
-
static getPositions(portfolio, userType) {
|
|
131
|
-
if (!portfolio) return []; // Handle empty portfolio
|
|
132
|
-
|
|
133
|
-
if (userType === SCHEMAS.USER_TYPES.SPECULATOR) {
|
|
134
|
-
return portfolio.PublicPositions || []; // SPECULATOR SCHEMA
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Default to Normal User Schema
|
|
138
|
-
return portfolio.AggregatedPositions || [];
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// ========================================================================
|
|
142
|
-
// 2. IDENTITY & KEYS
|
|
143
|
-
// ========================================================================
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Extract standardized Instrument ID.
|
|
147
|
-
*/
|
|
148
|
-
static getInstrumentId(position) {
|
|
149
|
-
if (!position) return null; // Handle empty position data
|
|
150
|
-
// Handle string or number variations safely
|
|
151
|
-
return position.InstrumentID || position.instrumentId || null;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Extract a unique Identifier for the position.
|
|
156
|
-
* - Speculator: Uses 'PositionID'.
|
|
157
|
-
* - Normal: Generates Composite Key (InstrumentID_Direction) since they lack unique Trade IDs.
|
|
158
|
-
*/
|
|
159
|
-
static getPositionId(position) {
|
|
160
|
-
if (!position) return null; // Handle empty position data
|
|
161
|
-
|
|
162
|
-
// 1. Try Explicit ID (Speculators)
|
|
163
|
-
if (position.PositionID) return String(position.PositionID);
|
|
164
|
-
if (position.PositionId) return String(position.PositionId);
|
|
165
|
-
|
|
166
|
-
// 2. Fallback to Composite Key (Normal Users)
|
|
167
|
-
const instId = this.getInstrumentId(position);
|
|
168
|
-
const dir = this.getDirection(position);
|
|
169
|
-
if (instId) return `${instId}_${dir}`;
|
|
170
|
-
|
|
171
|
-
return null;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// ========================================================================
|
|
175
|
-
// 3. FINANCIAL METRICS (WEIGHTS & P&L)
|
|
176
|
-
// ========================================================================
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Extract Net Profit %.
|
|
180
|
-
* Schema: 'NetProfit' is the percentage profit relative to invested capital.
|
|
181
|
-
*/
|
|
182
|
-
static getNetProfit(position) {
|
|
183
|
-
return position ? (position.NetProfit || 0) : 0;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Extract Position Weight (Allocation %).
|
|
188
|
-
* Schema:
|
|
189
|
-
* - Normal: 'Invested' is % of initial capital.
|
|
190
|
-
* - Speculator: 'Invested' (or 'Amount' in some contexts) is % of initial capital.
|
|
191
|
-
*/
|
|
192
|
-
static getPositionWeight(position, userType) { // Agnostic on user type, unused.
|
|
193
|
-
if (!position) return 0;
|
|
194
|
-
|
|
195
|
-
// Both schemas use 'Invested' to represent the allocation percentage.
|
|
196
|
-
// Speculators might optionally have 'Amount', we prioritize 'Invested' for consistency.
|
|
197
|
-
return position.Invested || position.Amount || 0;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Extract Current Equity Value %.
|
|
202
|
-
* Schema: 'Value' is the current value as a % of total portfolio equity.
|
|
203
|
-
*/
|
|
204
|
-
static getPositionValuePct(position) {
|
|
205
|
-
return position ? (position.Value || 0) : 0;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* --- NEW PRIMITIVE ---
|
|
210
|
-
* Derives the approximate Entry Price of a position based on Current Price and Net Profit %.
|
|
211
|
-
* Formula: Entry = Current / (1 + (NetProfit / 100))
|
|
212
|
-
* @param {number} currentPrice - The current market price of the asset.
|
|
213
|
-
* @param {number} netProfitPct - The Net Profit percentage (e.g., -20.5).
|
|
214
|
-
* @returns {number} Estimated Entry Price.
|
|
215
|
-
*/
|
|
216
|
-
static deriveEntryPrice(currentPrice, netProfitPct) {
|
|
217
|
-
if (!currentPrice || currentPrice <= 0) return 0;
|
|
218
|
-
// Avoid division by zero if P&L is -100% (unlikely but possible in crypto/options)
|
|
219
|
-
if (netProfitPct <= -100) return Number.MAX_SAFE_INTEGER; // Effectively infinite entry price (lost everything)
|
|
220
|
-
return currentPrice / (1 + (netProfitPct / 100.0));
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// ========================================================================
|
|
224
|
-
// 4. PORTFOLIO LEVEL SUMMARY
|
|
225
|
-
// ========================================================================
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Calculate/Extract Daily Portfolio P&L %.
|
|
229
|
-
*/
|
|
230
|
-
static getPortfolioDailyPnl(portfolio, userType) {
|
|
231
|
-
if (!portfolio) return 0;
|
|
232
|
-
|
|
233
|
-
// 1. Speculator (Explicit 'NetProfit' field on root)
|
|
234
|
-
if (userType === SCHEMAS.USER_TYPES.SPECULATOR) {
|
|
235
|
-
return portfolio.NetProfit || 0;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// 2. Normal (Aggregated Calculation)
|
|
239
|
-
if (portfolio.AggregatedPositionsByInstrumentTypeID) {
|
|
240
|
-
return portfolio.AggregatedPositionsByInstrumentTypeID.reduce((sum, agg) => {
|
|
241
|
-
return sum + ((agg.Value || 0) - (agg.Invested || 0));
|
|
242
|
-
}, 0);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return 0;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// ========================================================================
|
|
249
|
-
// 5. TRADE DETAILS (SPECULATOR SPECIFIC)
|
|
250
|
-
// ========================================================================
|
|
251
|
-
|
|
252
|
-
static getDirection(position) {
|
|
253
|
-
if (!position) return "Buy";
|
|
254
|
-
if (position.Direction) return position.Direction;
|
|
255
|
-
if (typeof position.IsBuy === 'boolean') return position.IsBuy ? "Buy" : "Sell";
|
|
256
|
-
return "Buy"; // Default
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
static getLeverage(position) {
|
|
260
|
-
return position ? (position.Leverage || 1) : 1; // Default 1 IF NOT FOUND
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
static getOpenRate(position) {
|
|
264
|
-
return position ? (position.OpenRate || 0) : 0; // Default 0 IF NOT FOUND
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
static getCurrentRate(position) {
|
|
268
|
-
return position ? (position.CurrentRate || 0) : 0; // Default 0 IF NOT FOUND
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
static getStopLossRate(position) {
|
|
272
|
-
const rate = position ? (position.StopLossRate || 0) : 0;
|
|
273
|
-
if (rate > 0 && rate <= 0.01) return 0; // Normalizes bug value to 0
|
|
274
|
-
if (rate < 0) return 0;
|
|
275
|
-
return rate;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
static getTakeProfitRate(position) {
|
|
279
|
-
const rate = position ? (position.TakeProfitRate || 0) : 0;
|
|
280
|
-
if (rate > 0 && rate <= 0.01) return 0; // Normalizes bug value to 0
|
|
281
|
-
return rate;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
static getHasTSL(position) {
|
|
285
|
-
return position ? (position.HasTrailingStopLoss === true) : false; // Default false IF NOT FOUND
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
static getOpenDateTime(position) {
|
|
289
|
-
if (!position || !position.OpenDateTime) return null;
|
|
290
|
-
return new Date(position.OpenDateTime);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
class priceExtractor {
|
|
295
|
-
static getHistory(pricesContext, tickerOrId) {
|
|
296
|
-
if (!pricesContext || !pricesContext.history) return [];
|
|
297
|
-
let assetData = pricesContext.history[tickerOrId];
|
|
298
|
-
|
|
299
|
-
if (!assetData) {
|
|
300
|
-
const id = Object.keys(pricesContext.history).find(key => {
|
|
301
|
-
const data = pricesContext.history[key];
|
|
302
|
-
return data.ticker === tickerOrId;
|
|
303
|
-
});
|
|
304
|
-
if (id) assetData = pricesContext.history[id];
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
if (!assetData || !assetData.prices) return [];
|
|
308
|
-
|
|
309
|
-
const priceMap = assetData.prices;
|
|
310
|
-
const sortedDates = Object.keys(priceMap).sort((a, b) => a.localeCompare(b));
|
|
311
|
-
|
|
312
|
-
return sortedDates.map(date => ({
|
|
313
|
-
date: date,
|
|
314
|
-
price: priceMap[date]
|
|
315
|
-
})).filter(item => item.price > 0);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
static getAllHistories(pricesContext) {
|
|
319
|
-
if (!pricesContext || !pricesContext.history) return new Map();
|
|
320
|
-
|
|
321
|
-
const results = new Map();
|
|
322
|
-
for (const [id, data] of Object.entries(pricesContext.history)) {
|
|
323
|
-
const ticker = data.ticker || id;
|
|
324
|
-
const history = this.getHistory(pricesContext, id);
|
|
325
|
-
if (history.length > 0) {
|
|
326
|
-
results.set(ticker, history);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
return results;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
class HistoryExtractor {
|
|
334
|
-
static getDailyHistory(user) {
|
|
335
|
-
return user?.history?.today || null;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
static getTradedAssets(historyDoc) {
|
|
339
|
-
const trades = historyDoc?.PublicHistoryPositions || [];
|
|
340
|
-
if (!trades.length) return [];
|
|
341
|
-
|
|
342
|
-
const assetsMap = new Map();
|
|
343
|
-
|
|
344
|
-
for (const t of trades) {
|
|
345
|
-
const instId = t.InstrumentID;
|
|
346
|
-
if (!instId) continue;
|
|
347
|
-
|
|
348
|
-
if (!assetsMap.has(instId)) {
|
|
349
|
-
assetsMap.set(instId, {
|
|
350
|
-
instrumentId: instId,
|
|
351
|
-
totalDuration: 0,
|
|
352
|
-
count: 0
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const asset = assetsMap.get(instId);
|
|
357
|
-
const open = new Date(t.OpenDateTime);
|
|
358
|
-
const close = new Date(t.CloseDateTime);
|
|
359
|
-
const durationMins = (close - open) / 60000;
|
|
360
|
-
|
|
361
|
-
if (durationMins > 0) {
|
|
362
|
-
asset.totalDuration += durationMins;
|
|
363
|
-
asset.count++;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
return Array.from(assetsMap.values()).map(a => ({
|
|
368
|
-
instrumentId: a.instrumentId,
|
|
369
|
-
avgHoldingTimeInMinutes: a.count > 0 ? (a.totalDuration / a.count) : 0
|
|
370
|
-
}));
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
static getInstrumentId(asset) {
|
|
374
|
-
return asset ? asset.instrumentId : null;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
static getAvgHoldingTimeMinutes(asset) {
|
|
378
|
-
return asset ? (asset.avgHoldingTimeInMinutes || 0) : 0;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
static getSummary(historyDoc) {
|
|
382
|
-
const trades = historyDoc?.PublicHistoryPositions || [];
|
|
383
|
-
if (!trades.length) return null;
|
|
384
|
-
|
|
385
|
-
let totalTrades = trades.length;
|
|
386
|
-
let wins = 0;
|
|
387
|
-
let totalProf = 0;
|
|
388
|
-
let totalLoss = 0;
|
|
389
|
-
let profCount = 0;
|
|
390
|
-
let lossCount = 0;
|
|
391
|
-
let totalDur = 0;
|
|
392
|
-
|
|
393
|
-
for (const t of trades) {
|
|
394
|
-
if (t.NetProfit > 0) {
|
|
395
|
-
wins++;
|
|
396
|
-
totalProf += t.NetProfit;
|
|
397
|
-
profCount++;
|
|
398
|
-
} else if (t.NetProfit < 0) {
|
|
399
|
-
totalLoss += t.NetProfit;
|
|
400
|
-
lossCount++;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
const open = new Date(t.OpenDateTime);
|
|
404
|
-
const close = new Date(t.CloseDateTime);
|
|
405
|
-
totalDur += (close - open) / 60000;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
return {
|
|
409
|
-
totalTrades: totalTrades,
|
|
410
|
-
winRatio: totalTrades > 0 ? (wins / totalTrades) * 100 : 0,
|
|
411
|
-
avgProfitPct: profCount > 0 ? totalProf / profCount : 0,
|
|
412
|
-
avgLossPct: lossCount > 0 ? totalLoss / lossCount : 0,
|
|
413
|
-
avgHoldingTimeInMinutes: totalTrades > 0 ? totalDur / totalTrades : 0
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
class SignalPrimitives {
|
|
419
|
-
static getMetric(dependencies, calcName, ticker, fieldName, fallback = 0) {
|
|
420
|
-
if (!dependencies || !dependencies[calcName]) return fallback;
|
|
421
|
-
const tickerData = dependencies[calcName][ticker];
|
|
422
|
-
if (!tickerData) return fallback;
|
|
423
|
-
|
|
424
|
-
const val = tickerData[fieldName];
|
|
425
|
-
return (typeof val === 'number') ? val : fallback;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
static getUnionKeys(dependencies, calcNames) {
|
|
429
|
-
const keys = new Set();
|
|
430
|
-
if (!dependencies) return [];
|
|
431
|
-
for (const name of calcNames) {
|
|
432
|
-
const resultObj = dependencies[name];
|
|
433
|
-
if (resultObj && typeof resultObj === 'object') {
|
|
434
|
-
Object.keys(resultObj).forEach(k => keys.add(k));
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
return Array.from(keys);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
static normalizeTanh(value, scale = 10, sensitivity = 10.0) {
|
|
441
|
-
if (value === 0) return 0;
|
|
442
|
-
return Math.tanh(value / sensitivity) * scale;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
static normalizeZScore(value, mean, stdDev) {
|
|
446
|
-
if (!stdDev || stdDev === 0) return 0;
|
|
447
|
-
return (value - mean) / stdDev;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
static divergence(valueA, valueB) {
|
|
451
|
-
return (valueA || 0) - (valueB || 0);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
static getPreviousState(previousComputed, calcName, ticker, fieldName = null) {
|
|
455
|
-
if (!previousComputed || !previousComputed[calcName]) return null;
|
|
456
|
-
const tickerData = previousComputed[calcName][ticker];
|
|
457
|
-
if (!tickerData) return null;
|
|
458
|
-
|
|
459
|
-
if (fieldName) {
|
|
460
|
-
return tickerData[fieldName];
|
|
461
|
-
}
|
|
462
|
-
return tickerData;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
class MathPrimitives {
|
|
467
|
-
static average(values) {
|
|
468
|
-
if (!values || !values.length) return 0;
|
|
469
|
-
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
static median(values) {
|
|
473
|
-
if (!values || !values.length) return 0;
|
|
474
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
475
|
-
const mid = Math.floor(sorted.length / 2);
|
|
476
|
-
return sorted.length % 2 === 0
|
|
477
|
-
? (sorted[mid - 1] + sorted[mid]) / 2
|
|
478
|
-
: sorted[mid];
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
static standardDeviation(values) {
|
|
482
|
-
if (!values || !values.length) return 0;
|
|
483
|
-
const avg = this.average(values);
|
|
484
|
-
const squareDiffs = values.map(val => Math.pow((val || 0) - avg, 2));
|
|
485
|
-
return Math.sqrt(this.average(squareDiffs));
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
static bucketBinary(value, threshold = 0) {
|
|
489
|
-
return value > threshold ? 'winner' : 'loser';
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
static calculateHitProbability(currentPrice, barrierPrice, volatility, days, drift = 0) {
|
|
493
|
-
if (currentPrice <= 0 || barrierPrice <= 0 || volatility <= 0 || days <= 0) return 0;
|
|
494
|
-
|
|
495
|
-
const t = days / 365.0;
|
|
496
|
-
const sigma = volatility;
|
|
497
|
-
const mu = drift;
|
|
498
|
-
const b = Math.log(barrierPrice / currentPrice);
|
|
499
|
-
const nu = mu - 0.5 * Math.pow(sigma, 2);
|
|
500
|
-
const sqrtT = Math.sqrt(t);
|
|
501
|
-
const sigmaSqrtT = sigma * sqrtT;
|
|
502
|
-
|
|
503
|
-
const normCDF = (x) => {
|
|
504
|
-
const t = 1 / (1 + 0.2316419 * Math.abs(x));
|
|
505
|
-
const d = 0.3989423 * Math.exp(-x * x / 2);
|
|
506
|
-
const prob = d * t * (0.3193815 + t * (-0.3565638 + t * (1.781478 + t * (-1.821256 + t * 1.330274))));
|
|
507
|
-
return x > 0 ? 1 - prob : prob;
|
|
508
|
-
};
|
|
509
|
-
|
|
510
|
-
const term1 = (b - nu * t) / sigmaSqrtT;
|
|
511
|
-
const term2 = (2 * nu * b) / (sigma * sigma);
|
|
512
|
-
const term3 = (b + nu * t) / sigmaSqrtT;
|
|
513
|
-
|
|
514
|
-
if ((currentPrice > barrierPrice && barrierPrice > currentPrice) ||
|
|
515
|
-
(currentPrice < barrierPrice && barrierPrice < currentPrice)) {
|
|
516
|
-
return 1.0;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
const probability = normCDF(( -Math.abs(b) - nu * t ) / sigmaSqrtT) + Math.exp((2 * nu * Math.abs(b)) / (sigma * sigma)) * normCDF(( -Math.abs(b) + nu * t ) / sigmaSqrtT);
|
|
520
|
-
|
|
521
|
-
return Math.min(Math.max(probability, 0), 1);
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
static simulateGBM(currentPrice, volatility, days, simulations = 1000, drift = 0) {
|
|
525
|
-
if (currentPrice <= 0 || volatility <= 0 || days <= 0) return new Float32Array(0);
|
|
526
|
-
|
|
527
|
-
const t = days / 365.0;
|
|
528
|
-
const sigma = volatility;
|
|
529
|
-
const mu = drift;
|
|
530
|
-
const driftTerm = (mu - 0.5 * sigma * sigma) * t;
|
|
531
|
-
const volTerm = sigma * Math.sqrt(t);
|
|
532
|
-
|
|
533
|
-
const results = new Float32Array(simulations);
|
|
534
|
-
|
|
535
|
-
for (let i = 0; i < simulations; i++) {
|
|
536
|
-
const u1 = Math.random();
|
|
537
|
-
const u2 = Math.random();
|
|
538
|
-
const z = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);
|
|
539
|
-
results[i] = currentPrice * Math.exp(driftTerm + volTerm * z);
|
|
540
|
-
}
|
|
541
|
-
return results;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
static simulatePopulationBreakdown(pricePaths, userProfiles) {
|
|
545
|
-
if (!pricePaths.length || !userProfiles.length) return 0;
|
|
546
|
-
|
|
547
|
-
let totalBreakdownEvents = 0;
|
|
548
|
-
const totalSims = pricePaths.length;
|
|
549
|
-
const totalUsers = userProfiles.length;
|
|
550
|
-
|
|
551
|
-
for (let i = 0; i < totalSims; i++) {
|
|
552
|
-
const simPrice = pricePaths[i];
|
|
553
|
-
let capitulatedUsersInScenario = 0;
|
|
554
|
-
|
|
555
|
-
for (let j = 0; j < totalUsers; j++) {
|
|
556
|
-
const user = userProfiles[j];
|
|
557
|
-
const hypotheticalPnL = ((simPrice - user.entryPrice) / user.entryPrice) * 100;
|
|
558
|
-
|
|
559
|
-
if (hypotheticalPnL < user.thresholdPct) {
|
|
560
|
-
capitulatedUsersInScenario++;
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
totalBreakdownEvents += (capitulatedUsersInScenario / totalUsers);
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
return totalBreakdownEvents / totalSims;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
class Aggregators {
|
|
572
|
-
static bucketUsersByPnlPerAsset(usersData, tickerMap) {
|
|
573
|
-
const buckets = new Map();
|
|
574
|
-
for (const [userId, portfolio] of Object.entries(usersData)) {
|
|
575
|
-
const userType = portfolio.PublicPositions ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL;
|
|
576
|
-
const positions = DataExtractor.getPositions(portfolio, userType);
|
|
577
|
-
|
|
578
|
-
for (const pos of positions) {
|
|
579
|
-
const id = DataExtractor.getInstrumentId(pos);
|
|
580
|
-
const pnl = DataExtractor.getNetProfit(pos);
|
|
581
|
-
if (!id || pnl === 0) continue;
|
|
582
|
-
|
|
583
|
-
const ticker = tickerMap[id];
|
|
584
|
-
if (!ticker) continue;
|
|
585
|
-
|
|
586
|
-
if (!buckets.has(ticker)) buckets.set(ticker, { winners: [], losers: [] });
|
|
587
|
-
const b = buckets.get(ticker);
|
|
588
|
-
|
|
589
|
-
if (pnl > 0) b.winners.push(userId);
|
|
590
|
-
else b.losers.push(userId);
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
return Object.fromEntries(buckets);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
static getWeightedSentiment(positions) {
|
|
597
|
-
if (!positions || positions.length === 0) return 0;
|
|
598
|
-
|
|
599
|
-
let totalWeightedPnL = 0;
|
|
600
|
-
let totalWeight = 0;
|
|
601
|
-
|
|
602
|
-
for (const pos of positions) {
|
|
603
|
-
const pnl = DataExtractor.getNetProfit(pos);
|
|
604
|
-
const weight = DataExtractor.getPositionWeight(pos);
|
|
605
|
-
|
|
606
|
-
if (weight > 0) {
|
|
607
|
-
totalWeightedPnL += (pnl * weight);
|
|
608
|
-
totalWeight += weight;
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
if (totalWeight === 0) return 0;
|
|
613
|
-
return totalWeightedPnL / totalWeight;
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
class Validators {
|
|
618
|
-
static validatePortfolio(portfolio, userType) {
|
|
619
|
-
if (!portfolio) return { valid: false, errors: ['Portfolio is null'] };
|
|
620
|
-
|
|
621
|
-
if (userType === SCHEMAS.USER_TYPES.SPECULATOR) {
|
|
622
|
-
if (!portfolio.PublicPositions) return { valid: false, errors: ['Missing PublicPositions'] };
|
|
623
|
-
} else {
|
|
624
|
-
if (!portfolio.AggregatedPositions) return { valid: false, errors: ['Missing AggregatedPositions'] };
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
return { valid: true, errors: [] };
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
class TimeSeries {
|
|
632
|
-
static updateEMAState(value, state, alpha = 0.1) {
|
|
633
|
-
const mean = state ? (state.mean || 0) : 0;
|
|
634
|
-
const variance = state ? (state.variance || 1) : 1;
|
|
635
|
-
|
|
636
|
-
if (value === undefined || value === null || isNaN(value)) {
|
|
637
|
-
return { mean, variance };
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
const diff = value - mean;
|
|
641
|
-
const newMean = mean + (alpha * diff);
|
|
642
|
-
const newVariance = (1 - alpha) * (variance + (alpha * diff * diff));
|
|
643
|
-
|
|
644
|
-
return { mean: newMean, variance: newVariance };
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
static pearsonCorrelation(x, y) {
|
|
648
|
-
if (!x || !y || x.length !== y.length || x.length === 0) return 0;
|
|
649
|
-
|
|
650
|
-
const n = x.length;
|
|
651
|
-
let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0, sumY2 = 0;
|
|
652
|
-
|
|
653
|
-
for (let i = 0; i < n; i++) {
|
|
654
|
-
sumX += x[i];
|
|
655
|
-
sumY += y[i];
|
|
656
|
-
sumXY += x[i] * y[i];
|
|
657
|
-
sumX2 += x[i] * x[i];
|
|
658
|
-
sumY2 += y[i] * y[i];
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
const numerator = (n * sumXY) - (sumX * sumY);
|
|
662
|
-
const denominator = Math.sqrt(((n * sumX2) - (sumX * sumX)) * ((n * sumY2) - (sumY * sumY)));
|
|
663
|
-
|
|
664
|
-
return (denominator === 0) ? 0 : numerator / denominator;
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
class DistributionAnalytics {
|
|
670
|
-
static computeKDE(data, bandwidth, steps = 60) {
|
|
671
|
-
if (!data || data.length === 0) return [];
|
|
672
|
-
|
|
673
|
-
let min = Infinity, max = -Infinity;
|
|
674
|
-
for (const p of data) {
|
|
675
|
-
if (p.value < min) min = p.value;
|
|
676
|
-
if (p.value > max) max = p.value;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
min -= bandwidth * 3;
|
|
680
|
-
max += bandwidth * 3;
|
|
681
|
-
const stepSize = (max - min) / steps;
|
|
682
|
-
const curve = [];
|
|
683
|
-
|
|
684
|
-
for (let i = 0; i <= steps; i++) {
|
|
685
|
-
const x = min + (i * stepSize);
|
|
686
|
-
let density = 0;
|
|
687
|
-
for (const p of data) {
|
|
688
|
-
const diff = (x - p.value);
|
|
689
|
-
if (Math.abs(diff) > bandwidth * 3) continue;
|
|
690
|
-
|
|
691
|
-
const u = diff / bandwidth;
|
|
692
|
-
const k = 0.39894228 * Math.exp(-0.5 * u * u);
|
|
693
|
-
density += (p.weight * k) / bandwidth;
|
|
694
|
-
}
|
|
695
|
-
if (density > 0) curve.push({ price: x, density });
|
|
696
|
-
}
|
|
697
|
-
return curve;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
static integrateProfile(curve, startPrice, endPrice) {
|
|
701
|
-
if (!curve || !Array.isArray(curve)) return 0; // Fix for potential crash
|
|
702
|
-
let sum = 0;
|
|
703
|
-
for (let i = 0; i < curve.length - 1; i++) {
|
|
704
|
-
const p1 = curve[i];
|
|
705
|
-
const p2 = curve[i+1];
|
|
706
|
-
if (p1.price >= startPrice && p2.price <= endPrice) {
|
|
707
|
-
sum += (p2.price - p1.price) * ((p1.density + p2.density) / 2);
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
return sum;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
static linearRegression(xValues, yValues) {
|
|
714
|
-
const n = xValues.length;
|
|
715
|
-
if (n !== yValues.length || n < 2) return { slope: 0, r2: 0 };
|
|
716
|
-
|
|
717
|
-
let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0, sumYY = 0;
|
|
718
|
-
for (let i = 0; i < n; i++) {
|
|
719
|
-
sumX += xValues[i];
|
|
720
|
-
sumY += yValues[i];
|
|
721
|
-
sumXY += xValues[i] * yValues[i];
|
|
722
|
-
sumXX += xValues[i] * xValues[i];
|
|
723
|
-
sumYY += yValues[i] * yValues[i];
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
|
|
727
|
-
return { slope, n };
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
module.exports = {
|
|
732
|
-
Aggregators,
|
|
733
|
-
DataExtractor,
|
|
734
|
-
DistributionAnalytics,
|
|
735
|
-
HistoryExtractor,
|
|
736
|
-
InsightsExtractor, // Exported
|
|
737
|
-
MathPrimitives,
|
|
738
|
-
SCHEMAS,
|
|
739
|
-
SignalPrimitives,
|
|
740
|
-
TimeSeries,
|
|
741
|
-
UserClassifier, // Exported
|
|
742
|
-
Validators,
|
|
743
|
-
priceExtractor
|
|
744
|
-
};
|