bulltrackers-module 1.0.172 → 1.0.174
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/README.MD +3 -179
- package/functions/computation-system/controllers/computation_controller.js +85 -89
- package/functions/computation-system/helpers/computation_manifest_builder.js +15 -20
- package/functions/computation-system/helpers/computation_pass_runner.js +68 -35
- package/functions/computation-system/helpers/orchestration_helpers.js +63 -58
- package/functions/computation-system/layers/math_primitives.js +445 -44
- package/index.js +1 -1
- package/package.json +1 -1
|
@@ -11,7 +11,7 @@ const SCHEMAS = {
|
|
|
11
11
|
USER_TYPES: { NORMAL: 'normal', SPECULATOR: 'speculator' }
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
class DataExtractor {
|
|
14
|
+
class DataExtractor { // For generic access of data types
|
|
15
15
|
// ========================================================================
|
|
16
16
|
// 1. COLLECTION ACCESSORS
|
|
17
17
|
// ========================================================================
|
|
@@ -22,10 +22,10 @@ class DataExtractor {
|
|
|
22
22
|
* - Speculator: Uses 'PublicPositions' (Individual Trades)
|
|
23
23
|
*/
|
|
24
24
|
static getPositions(portfolio, userType) {
|
|
25
|
-
if (!portfolio) return [];
|
|
25
|
+
if (!portfolio) return []; // Handle empty portfolio
|
|
26
26
|
|
|
27
|
-
if (userType === SCHEMAS.USER_TYPES.SPECULATOR) {
|
|
28
|
-
return portfolio.PublicPositions || [];
|
|
27
|
+
if (userType === SCHEMAS.USER_TYPES.SPECULATOR) {
|
|
28
|
+
return portfolio.PublicPositions || []; // SPECULATOR SCHEMA
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
// Default to Normal User Schema
|
|
@@ -40,7 +40,7 @@ class DataExtractor {
|
|
|
40
40
|
* Extract standardized Instrument ID.
|
|
41
41
|
*/
|
|
42
42
|
static getInstrumentId(position) {
|
|
43
|
-
if (!position) return null;
|
|
43
|
+
if (!position) return null; // Handle empty position data
|
|
44
44
|
// Handle string or number variations safely
|
|
45
45
|
return position.InstrumentID || position.instrumentId || null;
|
|
46
46
|
}
|
|
@@ -51,7 +51,7 @@ class DataExtractor {
|
|
|
51
51
|
* - Normal: Generates Composite Key (InstrumentID_Direction) since they lack unique Trade IDs.
|
|
52
52
|
*/
|
|
53
53
|
static getPositionId(position) {
|
|
54
|
-
if (!position) return null;
|
|
54
|
+
if (!position) return null; // Handle empty position data
|
|
55
55
|
|
|
56
56
|
// 1. Try Explicit ID (Speculators)
|
|
57
57
|
if (position.PositionID) return String(position.PositionID);
|
|
@@ -59,7 +59,7 @@ class DataExtractor {
|
|
|
59
59
|
|
|
60
60
|
// 2. Fallback to Composite Key (Normal Users)
|
|
61
61
|
const instId = this.getInstrumentId(position);
|
|
62
|
-
const dir
|
|
62
|
+
const dir = this.getDirection(position);
|
|
63
63
|
if (instId) return `${instId}_${dir}`;
|
|
64
64
|
|
|
65
65
|
return null;
|
|
@@ -73,7 +73,7 @@ class DataExtractor {
|
|
|
73
73
|
* Extract Net Profit %.
|
|
74
74
|
* Schema: 'NetProfit' is the percentage profit relative to invested capital.
|
|
75
75
|
*/
|
|
76
|
-
static getNetProfit(position) {
|
|
76
|
+
static getNetProfit(position) {
|
|
77
77
|
return position ? (position.NetProfit || 0) : 0;
|
|
78
78
|
}
|
|
79
79
|
|
|
@@ -83,7 +83,7 @@ class DataExtractor {
|
|
|
83
83
|
* - Normal: 'Invested' is % of initial capital.
|
|
84
84
|
* - Speculator: 'Invested' (or 'Amount' in some contexts) is % of initial capital.
|
|
85
85
|
*/
|
|
86
|
-
static getPositionWeight(position, userType) {
|
|
86
|
+
static getPositionWeight(position, userType) { // Agnostic on user type, unused.
|
|
87
87
|
if (!position) return 0;
|
|
88
88
|
|
|
89
89
|
// Both schemas use 'Invested' to represent the allocation percentage.
|
|
@@ -95,8 +95,23 @@ class DataExtractor {
|
|
|
95
95
|
* Extract Current Equity Value %.
|
|
96
96
|
* Schema: 'Value' is the current value as a % of total portfolio equity.
|
|
97
97
|
*/
|
|
98
|
-
static getPositionValuePct(position) {
|
|
99
|
-
return position ? (position.Value || 0) : 0;
|
|
98
|
+
static getPositionValuePct(position) { // TODO - VERIFY THIS WORKS FOR SPECULATORS,
|
|
99
|
+
return position ? (position.Value || 0) : 0; // IS VALUE ACTUALLY THE VALUE OF POSITION AS A % OF TOTAL PORTFOLIO EQUITY? IS IT THE SAME FOR NORMAL USERS?
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* --- NEW PRIMITIVE ---
|
|
104
|
+
* Derives the approximate Entry Price of a position based on Current Price and Net Profit %.
|
|
105
|
+
* Formula: Entry = Current / (1 + (NetProfit / 100))
|
|
106
|
+
* @param {number} currentPrice - The current market price of the asset.
|
|
107
|
+
* @param {number} netProfitPct - The Net Profit percentage (e.g., -20.5).
|
|
108
|
+
* @returns {number} Estimated Entry Price.
|
|
109
|
+
*/
|
|
110
|
+
static deriveEntryPrice(currentPrice, netProfitPct) {
|
|
111
|
+
if (!currentPrice || currentPrice <= 0) return 0;
|
|
112
|
+
// Avoid division by zero if P&L is -100% (unlikely but possible in crypto/options)
|
|
113
|
+
if (netProfitPct <= -100) return Number.MAX_SAFE_INTEGER; // Effectively infinite entry price (lost everything)
|
|
114
|
+
return currentPrice / (1 + (netProfitPct / 100.0));
|
|
100
115
|
}
|
|
101
116
|
|
|
102
117
|
// ========================================================================
|
|
@@ -109,12 +124,13 @@ class DataExtractor {
|
|
|
109
124
|
static getPortfolioDailyPnl(portfolio, userType) {
|
|
110
125
|
if (!portfolio) return 0;
|
|
111
126
|
|
|
112
|
-
// 1. Speculator (Explicit 'NetProfit' field on root)
|
|
113
|
-
if (userType === SCHEMAS.USER_TYPES.SPECULATOR) {
|
|
114
|
-
|
|
127
|
+
// 1. Speculator (Explicit 'NetProfit' field on root) ---> TODO : THIS IS FLAWED,
|
|
128
|
+
if (userType === SCHEMAS.USER_TYPES.SPECULATOR) { // SPECULATOR DATA DOES NOT CONTAIN THE ENTIRE PORTFOLIO DATA FOR THAT USER, IT CONTAINS A SNAPSHOT OF THE USERS' SPECIFIC ASSET WE REQUESTED FROM THE TASK ENGINE
|
|
129
|
+
// You cannot ever infer the entire speculator portfolio daily / weekly / monthly or any time period value based on the data we have for speculators
|
|
130
|
+
return portfolio.NetProfit || 0; // Data for speculators only works when we are looking at the ticker-specific data, or the trade history data which is agnostic on user type
|
|
115
131
|
}
|
|
116
132
|
|
|
117
|
-
// 2. Normal (Aggregated Calculation)
|
|
133
|
+
// 2. Normal (Aggregated Calculation) ---> TODO : VERIFY THIS LOGIC, IT SHOULD BE OK
|
|
118
134
|
// Logic: Sum(Value - Invested) across the 'InstrumentType' breakdown
|
|
119
135
|
// This gives the net performance change for the day relative to the portfolio.
|
|
120
136
|
if (portfolio.AggregatedPositionsByInstrumentTypeID) {
|
|
@@ -138,27 +154,36 @@ class DataExtractor {
|
|
|
138
154
|
}
|
|
139
155
|
|
|
140
156
|
static getLeverage(position) {
|
|
141
|
-
return position ? (position.Leverage || 1) : 1;
|
|
157
|
+
return position ? (position.Leverage || 1) : 1; // Default 1 IF NOT FOUND
|
|
142
158
|
}
|
|
143
159
|
|
|
144
160
|
static getOpenRate(position) {
|
|
145
|
-
return position ? (position.OpenRate || 0) : 0;
|
|
161
|
+
return position ? (position.OpenRate || 0) : 0; // Default 0 IF NOT FOUND
|
|
146
162
|
}
|
|
147
163
|
|
|
148
164
|
static getCurrentRate(position) {
|
|
149
|
-
return position ? (position.CurrentRate || 0) : 0;
|
|
165
|
+
return position ? (position.CurrentRate || 0) : 0; // Default 0 IF NOT FOUND
|
|
150
166
|
}
|
|
151
167
|
|
|
152
168
|
static getStopLossRate(position) {
|
|
153
|
-
|
|
154
|
-
|
|
169
|
+
const rate = position ? (position.StopLossRate || 0) : 0;
|
|
170
|
+
// Fix for eToro bug: SL disabled positions can return 0.01 or similar small values.
|
|
171
|
+
// If the rate is positive but extremely small (<= 0.01), treat as 0 (disabled).
|
|
172
|
+
if (rate > 0 && rate <= 0.01) return 0; // Normalizes bug value to 0
|
|
173
|
+
if (rate < 0) return 0;
|
|
174
|
+
return rate;
|
|
175
|
+
}
|
|
155
176
|
|
|
156
177
|
static getTakeProfitRate(position) {
|
|
157
|
-
|
|
178
|
+
const rate = position ? (position.TakeProfitRate || 0) : 0;
|
|
179
|
+
// Fix for eToro bug: TP disabled positions can return INF or small values.
|
|
180
|
+
// If the rate is positive but extremely small (<= 0.01), treat as 0 (disabled).
|
|
181
|
+
if (rate > 0 && rate <= 0.01) return 0; // Normalizes bug value to 0
|
|
182
|
+
return rate;
|
|
158
183
|
}
|
|
159
184
|
|
|
160
185
|
static getHasTSL(position) {
|
|
161
|
-
return position ? (position.HasTrailingStopLoss === true) : false;
|
|
186
|
+
return position ? (position.HasTrailingStopLoss === true) : false; // Default false IF NOT FOUND
|
|
162
187
|
}
|
|
163
188
|
|
|
164
189
|
/**
|
|
@@ -166,11 +191,76 @@ class DataExtractor {
|
|
|
166
191
|
* Used for bagholder calculations.
|
|
167
192
|
*/
|
|
168
193
|
static getOpenDateTime(position) {
|
|
169
|
-
if (!position || !position.OpenDateTime) return null;
|
|
194
|
+
if (!position || !position.OpenDateTime) return null;
|
|
170
195
|
return new Date(position.OpenDateTime);
|
|
171
196
|
}
|
|
172
197
|
}
|
|
173
198
|
|
|
199
|
+
/**
|
|
200
|
+
* --- NEW CLASS: priceExtractor ---
|
|
201
|
+
* Handles schema-aware extraction of asset price history.
|
|
202
|
+
* Mitigates typo risk by centralizing access to context.prices.
|
|
203
|
+
*/
|
|
204
|
+
class priceExtractor {
|
|
205
|
+
/**
|
|
206
|
+
* Retrieves the sorted price history for a specific instrument.
|
|
207
|
+
* @param {object} pricesContext - The global context.prices object.
|
|
208
|
+
* @param {string|number} tickerOrId - The Ticker or InstrumentID to fetch.
|
|
209
|
+
* @returns {Array<{date: string, price: number}>} Sorted array of price objects.
|
|
210
|
+
*/
|
|
211
|
+
static getHistory(pricesContext, tickerOrId) {
|
|
212
|
+
if (!pricesContext || !pricesContext.history) return [];
|
|
213
|
+
|
|
214
|
+
// The history map is keyed by InstrumentID (e.g., "10000")
|
|
215
|
+
// We iterate to find the entry matching the request (by ID or Ticker)
|
|
216
|
+
// Optimization: If input is ID, direct lookup. If ticker, search.
|
|
217
|
+
|
|
218
|
+
let assetData = pricesContext.history[tickerOrId];
|
|
219
|
+
|
|
220
|
+
// Fallback: If direct lookup failed, maybe it's a ticker symbol?
|
|
221
|
+
if (!assetData) {
|
|
222
|
+
const id = Object.keys(pricesContext.history).find(key => {
|
|
223
|
+
const data = pricesContext.history[key];
|
|
224
|
+
return data.ticker === tickerOrId;
|
|
225
|
+
});
|
|
226
|
+
if (id) assetData = pricesContext.history[id];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!assetData || !assetData.prices) return [];
|
|
230
|
+
|
|
231
|
+
// Convert Map<Date, Price> to Array<{date, price}> and sort
|
|
232
|
+
const priceMap = assetData.prices;
|
|
233
|
+
const sortedDates = Object.keys(priceMap).sort((a, b) => a.localeCompare(b));
|
|
234
|
+
|
|
235
|
+
return sortedDates.map(date => ({
|
|
236
|
+
date: date,
|
|
237
|
+
price: priceMap[date]
|
|
238
|
+
})).filter(item => item.price > 0);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Returns all available assets with their histories.
|
|
243
|
+
* Useful for computations iterating over the entire market.
|
|
244
|
+
* @param {object} pricesContext - The global context.prices object.
|
|
245
|
+
* @returns {Map<string, Array<{date: string, price: number}>>} Map<Ticker, HistoryArray>
|
|
246
|
+
*/
|
|
247
|
+
static getAllHistories(pricesContext) {
|
|
248
|
+
if (!pricesContext || !pricesContext.history) return new Map();
|
|
249
|
+
|
|
250
|
+
const results = new Map();
|
|
251
|
+
for (const [id, data] of Object.entries(pricesContext.history)) {
|
|
252
|
+
const ticker = data.ticker || id;
|
|
253
|
+
// Reuse the single-asset logic for consistency, though slightly less efficient
|
|
254
|
+
// than inlining the sort. For safety/DRY, we reuse.
|
|
255
|
+
const history = this.getHistory(pricesContext, id);
|
|
256
|
+
if (history.length > 0) {
|
|
257
|
+
results.set(ticker, history);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return results;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
174
264
|
class HistoryExtractor {
|
|
175
265
|
// --- Schema Accessor (NEW) ---
|
|
176
266
|
/**
|
|
@@ -187,23 +277,23 @@ class HistoryExtractor {
|
|
|
187
277
|
return historyDoc.assets;
|
|
188
278
|
}
|
|
189
279
|
|
|
190
|
-
static getInstrumentId(asset) {
|
|
280
|
+
static getInstrumentId(asset) {
|
|
191
281
|
return asset ? asset.instrumentId : null;
|
|
192
282
|
}
|
|
193
283
|
|
|
194
|
-
static getAvgHoldingTimeMinutes(asset) {
|
|
284
|
+
static getAvgHoldingTimeMinutes(asset) { // Note, in minutes, we could convert values here into hours or days but we leave as-is for now.
|
|
195
285
|
return asset ? (asset.avgHoldingTimeInMinutes || 0) : 0;
|
|
196
286
|
}
|
|
197
287
|
|
|
198
|
-
static getSummary(historyDoc) {
|
|
288
|
+
static getSummary(historyDoc) { // This returns the top-level summary of trade history
|
|
199
289
|
const all = historyDoc?.all;
|
|
200
290
|
if (!all) return null;
|
|
201
291
|
|
|
202
|
-
return {
|
|
203
|
-
totalTrades:
|
|
204
|
-
winRatio:
|
|
205
|
-
avgProfitPct:
|
|
206
|
-
avgLossPct:
|
|
292
|
+
return { // The all object contains instrumentid of -1 value, we do not include this, it's a junk backend-eToro placeholder.
|
|
293
|
+
totalTrades: all.totalTrades || 0,
|
|
294
|
+
winRatio: all.winRatio || 0,
|
|
295
|
+
avgProfitPct: all.avgProfitPct || 0,
|
|
296
|
+
avgLossPct: all.avgLossPct || 0,
|
|
207
297
|
avgHoldingTimeInMinutes: all.avgHoldingTimeInMinutes || 0
|
|
208
298
|
};
|
|
209
299
|
}
|
|
@@ -242,7 +332,7 @@ class SignalPrimitives {
|
|
|
242
332
|
* Hyperbolic Tangent Normalization.
|
|
243
333
|
* Maps inputs to a strict -Scale to +Scale range.
|
|
244
334
|
*/
|
|
245
|
-
static normalizeTanh(value, scale = 10, sensitivity = 10.0) {
|
|
335
|
+
static normalizeTanh(value, scale = 10, sensitivity = 10.0) { // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/tanh
|
|
246
336
|
if (value === 0) return 0;
|
|
247
337
|
return Math.tanh(value / sensitivity) * scale;
|
|
248
338
|
}
|
|
@@ -250,7 +340,7 @@ class SignalPrimitives {
|
|
|
250
340
|
/**
|
|
251
341
|
* Standard Z-Score normalization.
|
|
252
342
|
*/
|
|
253
|
-
static normalizeZScore(value, mean, stdDev) {
|
|
343
|
+
static normalizeZScore(value, mean, stdDev) { // https://gist.github.com/Restuta/5cbf1d186f17febe5e899febb63e1b86
|
|
254
344
|
if (!stdDev || stdDev === 0) return 0;
|
|
255
345
|
return (value - mean) / stdDev;
|
|
256
346
|
}
|
|
@@ -261,6 +351,18 @@ class SignalPrimitives {
|
|
|
261
351
|
static divergence(valueA, valueB) {
|
|
262
352
|
return (valueA || 0) - (valueB || 0);
|
|
263
353
|
}
|
|
354
|
+
|
|
355
|
+
static getPreviousState(previousComputed, calcName, ticker, fieldName = null) { // This is used for either fetching computations listed in getdependencies() OR self-history
|
|
356
|
+
if (!previousComputed || !previousComputed[calcName]) return null; // Using this for self-history DOES NOT cause a circular dependency because we assign a special rule in orchestration_helpers
|
|
357
|
+
// Which handles the self-reference, see 2. SMART SELF-FETCH in orchestration_helpers
|
|
358
|
+
const tickerData = previousComputed[calcName][ticker];
|
|
359
|
+
if (!tickerData) return null;
|
|
360
|
+
|
|
361
|
+
if (fieldName) {
|
|
362
|
+
return tickerData[fieldName];
|
|
363
|
+
}
|
|
364
|
+
return tickerData; // Return whole object (useful for _state)
|
|
365
|
+
}
|
|
264
366
|
}
|
|
265
367
|
|
|
266
368
|
class MathPrimitives {
|
|
@@ -288,6 +390,146 @@ class MathPrimitives {
|
|
|
288
390
|
static bucketBinary(value, threshold = 0) {
|
|
289
391
|
return value > threshold ? 'winner' : 'loser';
|
|
290
392
|
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Calculates the probability of an asset hitting a specific price barrier (Stop Loss/Take Profit)
|
|
396
|
+
* within a given timeframe using the First Passage Time of Geometric Brownian Motion.
|
|
397
|
+
* * Formula: P(T < t) = Φ((b - vt) / σ√t) + exp(2vb/σ²) * Φ((b + vt) / σ√t)
|
|
398
|
+
* Where:
|
|
399
|
+
* b = ln(Barrier/Price)
|
|
400
|
+
* v = drift - 0.5 * volatility^2
|
|
401
|
+
* * @param {number} currentPrice - The current price of the asset
|
|
402
|
+
* @param {number} barrierPrice - The target price (SL or TP)
|
|
403
|
+
* @param {number} volatility - Annualized volatility (e.g., 0.40 for 40%)
|
|
404
|
+
* @param {number} days - Number of days to forecast (e.g., 3)
|
|
405
|
+
* @param {number} drift - (Optional) Annualized drift. Default 0 (Risk Neutral).
|
|
406
|
+
* @returns {number} Probability (0.0 to 1.0)
|
|
407
|
+
*/
|
|
408
|
+
static calculateHitProbability(currentPrice, barrierPrice, volatility, days, drift = 0) { // https://www.ma.ic.ac.uk/~bin06/M3A22/m3f22chVII.pdf
|
|
409
|
+
if (currentPrice <= 0 || barrierPrice <= 0 || volatility <= 0 || days <= 0) return 0;
|
|
410
|
+
|
|
411
|
+
const t = days / 365.0; // Convert days to years
|
|
412
|
+
const sigma = volatility;
|
|
413
|
+
const mu = drift;
|
|
414
|
+
|
|
415
|
+
// The barrier in log-space
|
|
416
|
+
const b = Math.log(barrierPrice / currentPrice);
|
|
417
|
+
|
|
418
|
+
// Adjusted drift (nu)
|
|
419
|
+
const nu = mu - 0.5 * Math.pow(sigma, 2);
|
|
420
|
+
|
|
421
|
+
const sqrtT = Math.sqrt(t);
|
|
422
|
+
const sigmaSqrtT = sigma * sqrtT;
|
|
423
|
+
|
|
424
|
+
// Helper for Standard Normal CDF (Φ)
|
|
425
|
+
const normCDF = (x) => {
|
|
426
|
+
const t = 1 / (1 + 0.2316419 * Math.abs(x));
|
|
427
|
+
const d = 0.3989423 * Math.exp(-x * x / 2);
|
|
428
|
+
const prob = d * t * (0.3193815 + t * (-0.3565638 + t * (1.781478 + t * (-1.821256 + t * 1.330274))));
|
|
429
|
+
return x > 0 ? 1 - prob : prob;
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// Standard First Passage Time Formula parts
|
|
433
|
+
const term1 = (b - nu * t) / sigmaSqrtT;
|
|
434
|
+
const term2 = (2 * nu * b) / (sigma * sigma);
|
|
435
|
+
const term3 = (b + nu * t) / sigmaSqrtT;
|
|
436
|
+
|
|
437
|
+
// If barrier is below price (Stop Loss for Long), b is negative.
|
|
438
|
+
// If barrier is above price (Take Profit for Long), we flip the logic essentially,
|
|
439
|
+
// but the formula works for the distance.
|
|
440
|
+
// However, for strict GBM hitting time, we usually treat 'b' as the distance.
|
|
441
|
+
// For this implementation, we check direction relative to barrier.
|
|
442
|
+
|
|
443
|
+
// If we are already at or past the barrier, probability is 100%
|
|
444
|
+
if ((currentPrice > barrierPrice && barrierPrice > currentPrice) ||
|
|
445
|
+
(currentPrice < barrierPrice && barrierPrice < currentPrice)) {
|
|
446
|
+
return 1.0;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Calculate Probability
|
|
450
|
+
// Note: If nu is 0, the second term simplifies significantly, but we keep full form.
|
|
451
|
+
const probability = normCDF(( -Math.abs(b) - nu * t ) / sigmaSqrtT) +
|
|
452
|
+
Math.exp((2 * nu * Math.abs(b)) / (sigma * sigma)) * normCDF(( -Math.abs(b) + nu * t ) / sigmaSqrtT);
|
|
453
|
+
|
|
454
|
+
return Math.min(Math.max(probability, 0), 1);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* --- NEW PRIMITIVE ---
|
|
459
|
+
* Simulates future price paths using Geometric Brownian Motion (Monte Carlo).
|
|
460
|
+
* Used for testing portfolio resilience against potential market moves.
|
|
461
|
+
* @param {number} currentPrice - S0
|
|
462
|
+
* @param {number} volatility - Annualized volatility (sigma)
|
|
463
|
+
* @param {number} days - Time horizon in days (t)
|
|
464
|
+
* @param {number} simulations - Number of paths to generate (e.g., 1000)
|
|
465
|
+
* @param {number} drift - Annualized drift (mu), default 0
|
|
466
|
+
* @returns {Float32Array} Array of simulated end prices
|
|
467
|
+
*/
|
|
468
|
+
static simulateGBM(currentPrice, volatility, days, simulations = 1000, drift = 0) {
|
|
469
|
+
if (currentPrice <= 0 || volatility <= 0 || days <= 0) return new Float32Array(0);
|
|
470
|
+
|
|
471
|
+
const t = days / 365.0;
|
|
472
|
+
const sigma = volatility;
|
|
473
|
+
const mu = drift;
|
|
474
|
+
const driftTerm = (mu - 0.5 * sigma * sigma) * t;
|
|
475
|
+
const volTerm = sigma * Math.sqrt(t);
|
|
476
|
+
|
|
477
|
+
// Use Float32Array for memory efficiency with large simulation counts
|
|
478
|
+
const results = new Float32Array(simulations);
|
|
479
|
+
|
|
480
|
+
for (let i = 0; i < simulations; i++) {
|
|
481
|
+
// Box-Muller transform for efficient standard normal distribution generation
|
|
482
|
+
const u1 = Math.random();
|
|
483
|
+
const u2 = Math.random();
|
|
484
|
+
const z = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);
|
|
485
|
+
|
|
486
|
+
// GBM Formula: St = S0 * exp((mu - 0.5*sigma^2)t + sigma*Wt)
|
|
487
|
+
results[i] = currentPrice * Math.exp(driftTerm + volTerm * z);
|
|
488
|
+
}
|
|
489
|
+
return results;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* --- NEW PRIMITIVE ---
|
|
494
|
+
* Simulates "Population Breakdown" (Capitulation) Risk.
|
|
495
|
+
* Correlates simulated price drops with user pain thresholds.
|
|
496
|
+
* @param {Float32Array} pricePaths - Array of simulated prices (from simulateGBM).
|
|
497
|
+
* @param {Array<{entryPrice: number, thresholdPct: number}>} userProfiles - Array of user states.
|
|
498
|
+
* @returns {number} Expected % of population that capitulates (0.0 - 1.0).
|
|
499
|
+
*/
|
|
500
|
+
static simulatePopulationBreakdown(pricePaths, userProfiles) {
|
|
501
|
+
if (!pricePaths.length || !userProfiles.length) return 0;
|
|
502
|
+
|
|
503
|
+
let totalBreakdownEvents = 0;
|
|
504
|
+
const totalSims = pricePaths.length;
|
|
505
|
+
const totalUsers = userProfiles.length;
|
|
506
|
+
|
|
507
|
+
// For each simulated future price scenario...
|
|
508
|
+
for (let i = 0; i < totalSims; i++) {
|
|
509
|
+
const simPrice = pricePaths[i];
|
|
510
|
+
let capitulatedUsersInScenario = 0;
|
|
511
|
+
|
|
512
|
+
// ...check every user to see if they survive
|
|
513
|
+
for (let j = 0; j < totalUsers; j++) {
|
|
514
|
+
const user = userProfiles[j];
|
|
515
|
+
// Calculate hypothetical P&L for this user in this scenario
|
|
516
|
+
// P&L% = (CurrentValue - EntryValue) / EntryValue
|
|
517
|
+
const hypotheticalPnL = ((simPrice - user.entryPrice) / user.entryPrice) * 100;
|
|
518
|
+
|
|
519
|
+
// If hypothetical P&L is worse (lower) than their historical pain threshold, they capitulate.
|
|
520
|
+
// Note: thresholdPct is typically negative (e.g., -15.0)
|
|
521
|
+
if (hypotheticalPnL < user.thresholdPct) {
|
|
522
|
+
capitulatedUsersInScenario++;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Add the % of users who broke in this scenario to the accumulator
|
|
527
|
+
totalBreakdownEvents += (capitulatedUsersInScenario / totalUsers);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Return the average capitulation rate across all simulations
|
|
531
|
+
return totalBreakdownEvents / totalSims;
|
|
532
|
+
}
|
|
291
533
|
}
|
|
292
534
|
|
|
293
535
|
class Aggregators {
|
|
@@ -295,11 +537,11 @@ class Aggregators {
|
|
|
295
537
|
* Helper to bucket users by P&L Status.
|
|
296
538
|
* Used by legacy systems or specific aggregators.
|
|
297
539
|
*/
|
|
298
|
-
static bucketUsersByPnlPerAsset(usersData, tickerMap) {
|
|
540
|
+
static bucketUsersByPnlPerAsset(usersData, tickerMap) { // https://www.geeksforgeeks.org/javascript/bucket-sort-visualization-using-javascript/
|
|
299
541
|
const buckets = new Map();
|
|
300
542
|
for (const [userId, portfolio] of Object.entries(usersData)) {
|
|
301
|
-
// Auto-detect type if not provided (Legacy compatibility)
|
|
302
|
-
const userType
|
|
543
|
+
// Auto-detect type if not provided (Legacy compatibility) TODO : We do not need legacy compatability, legacy computations do not run.
|
|
544
|
+
const userType = portfolio.PublicPositions ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL;
|
|
303
545
|
const positions = DataExtractor.getPositions(portfolio, userType);
|
|
304
546
|
|
|
305
547
|
for (const pos of positions) {
|
|
@@ -319,12 +561,42 @@ class Aggregators {
|
|
|
319
561
|
}
|
|
320
562
|
return Object.fromEntries(buckets);
|
|
321
563
|
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* --- NEW PRIMITIVE ---
|
|
567
|
+
* Calculates Weighted Sentiment (Avg P&L) for a set of positions.
|
|
568
|
+
* Solves "Dirty Data" / Variable N issue by weighting by investment size.
|
|
569
|
+
* @param {Array} positions - Array of raw position objects.
|
|
570
|
+
* @returns {number} Weighted Average NetProfit %.
|
|
571
|
+
*/
|
|
572
|
+
static getWeightedSentiment(positions) {
|
|
573
|
+
if (!positions || positions.length === 0) return 0;
|
|
574
|
+
|
|
575
|
+
let totalWeightedPnL = 0;
|
|
576
|
+
let totalWeight = 0;
|
|
577
|
+
|
|
578
|
+
for (const pos of positions) {
|
|
579
|
+
// Use DataExtractor to be safe and schema-agnostic
|
|
580
|
+
const pnl = DataExtractor.getNetProfit(pos);
|
|
581
|
+
const weight = DataExtractor.getPositionWeight(pos); // 'Invested' or 'Amount'
|
|
582
|
+
|
|
583
|
+
if (weight > 0) {
|
|
584
|
+
totalWeightedPnL += (pnl * weight);
|
|
585
|
+
totalWeight += weight;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (totalWeight === 0) return 0;
|
|
590
|
+
return totalWeightedPnL / totalWeight;
|
|
591
|
+
}
|
|
322
592
|
}
|
|
323
593
|
|
|
594
|
+
// Validation layer -- Used to validate the data incoming
|
|
324
595
|
class Validators {
|
|
325
596
|
static validatePortfolio(portfolio, userType) {
|
|
326
597
|
if (!portfolio) return { valid: false, errors: ['Portfolio is null'] };
|
|
327
598
|
|
|
599
|
+
// Handle both types of schema
|
|
328
600
|
if (userType === SCHEMAS.USER_TYPES.SPECULATOR) {
|
|
329
601
|
if (!portfolio.PublicPositions) return { valid: false, errors: ['Missing PublicPositions'] };
|
|
330
602
|
} else {
|
|
@@ -335,12 +607,141 @@ class Validators {
|
|
|
335
607
|
}
|
|
336
608
|
}
|
|
337
609
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
610
|
+
class TimeSeries {
|
|
611
|
+
/**
|
|
612
|
+
* Updates a Rolling Mean and Variance using Welford's Online Algorithm (EMA variant). // https://jonisalonen.com/2013/deriving-welfords-method-for-computing-variance/
|
|
613
|
+
* @param {number} value - New data point.
|
|
614
|
+
* @param {Object} state - { mean: number, variance: number }
|
|
615
|
+
* @param {number} alpha - Decay factor (e.g., 0.1 for ~20 days).
|
|
616
|
+
*/
|
|
617
|
+
static updateEMAState(value, state, alpha = 0.1) {
|
|
618
|
+
const mean = state ? (state.mean || 0) : 0;
|
|
619
|
+
const variance = state ? (state.variance || 1) : 1; // Default variance 1 to avoid div/0
|
|
620
|
+
|
|
621
|
+
if (value === undefined || value === null || isNaN(value)) {
|
|
622
|
+
return { mean, variance };
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// EMA Update for Mean
|
|
626
|
+
const diff = value - mean;
|
|
627
|
+
const newMean = mean + (alpha * diff);
|
|
628
|
+
|
|
629
|
+
// EMA Update for Variance
|
|
630
|
+
const newVariance = (1 - alpha) * (variance + (alpha * diff * diff));
|
|
631
|
+
|
|
632
|
+
return { mean: newMean, variance: newVariance };
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Calculates Pearson Correlation between two arrays. https://gist.github.com/matt-west/6500993
|
|
637
|
+
*/
|
|
638
|
+
static pearsonCorrelation(x, y) {
|
|
639
|
+
if (!x || !y || x.length !== y.length || x.length === 0) return 0;
|
|
640
|
+
|
|
641
|
+
const n = x.length;
|
|
642
|
+
// Simple sums
|
|
643
|
+
let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0, sumY2 = 0;
|
|
644
|
+
|
|
645
|
+
for (let i = 0; i < n; i++) {
|
|
646
|
+
sumX += x[i];
|
|
647
|
+
sumY += y[i];
|
|
648
|
+
sumXY += x[i] * y[i];
|
|
649
|
+
sumX2 += x[i] * x[i];
|
|
650
|
+
sumY2 += y[i] * y[i];
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const numerator = (n * sumXY) - (sumX * sumY);
|
|
654
|
+
const denominator = Math.sqrt(((n * sumX2) - (sumX * sumX)) * ((n * sumY2) - (sumY * sumY)));
|
|
655
|
+
|
|
656
|
+
return (denominator === 0) ? 0 : numerator / denominator;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
class DistributionAnalytics {
|
|
662
|
+
/**
|
|
663
|
+
* Gaussian Kernel Density Estimation (KDE)
|
|
664
|
+
* Converts discrete price points into a continuous probability density curve.
|
|
665
|
+
* Optimized for memory: accepts pre-binned data.
|
|
666
|
+
* @param {Array<{value: number, weight: number}>} data - Points or Bins.
|
|
667
|
+
* @param {number} bandwidth - Smoothing factor (h).
|
|
668
|
+
* @param {number} steps - Resolution of the output curve.
|
|
669
|
+
*/
|
|
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
|
+
// Pad range to capture tails
|
|
680
|
+
min -= bandwidth * 3;
|
|
681
|
+
max += bandwidth * 3;
|
|
682
|
+
const stepSize = (max - min) / steps;
|
|
683
|
+
const curve = [];
|
|
684
|
+
|
|
685
|
+
for (let i = 0; i <= steps; i++) {
|
|
686
|
+
const x = min + (i * stepSize);
|
|
687
|
+
let density = 0;
|
|
688
|
+
// Vectorized-like summation https://cvw.cac.cornell.edu/vector/intro/how-vector-works#:~:text=Vectorization%20is%20a%20process%20by,performance%20increases%20obtained%20by%20vectorization.
|
|
689
|
+
for (const p of data) {
|
|
690
|
+
const diff = (x - p.value);
|
|
691
|
+
// Optimization: Skip calculation for points too far away (> 3 std devs)
|
|
692
|
+
if (Math.abs(diff) > bandwidth * 3) continue;
|
|
693
|
+
|
|
694
|
+
const u = diff / bandwidth;
|
|
695
|
+
const k = 0.39894228 * Math.exp(-0.5 * u * u); // Standard Normal PDF
|
|
696
|
+
density += (p.weight * k) / bandwidth;
|
|
697
|
+
}
|
|
698
|
+
if (density > 0) curve.push({ price: x, density });
|
|
699
|
+
}
|
|
700
|
+
return curve;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
static integrateProfile(curve, startPrice, endPrice) {
|
|
704
|
+
let sum = 0;
|
|
705
|
+
for (let i = 0; i < curve.length - 1; i++) {
|
|
706
|
+
const p1 = curve[i];
|
|
707
|
+
const p2 = curve[i+1];
|
|
708
|
+
if (p1.price >= startPrice && p2.price <= endPrice) {
|
|
709
|
+
// Trapezoidal Rule https://www.khanacademy.org/math/ap-calculus-ab/ab-integration-new/ab-6-2/a/understanding-the-trapezoid-rule
|
|
710
|
+
sum += (p2.price - p1.price) * ((p1.density + p2.density) / 2);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
return sum;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
static linearRegression(xValues, yValues) { // https://hazlo.medium.com/linear-regression-from-scratch-in-js-first-foray-into-ml-for-web-developers-867cfcae8fde
|
|
717
|
+
const n = xValues.length;
|
|
718
|
+
if (n !== yValues.length || n < 2) return { slope: 0, r2: 0 };
|
|
719
|
+
|
|
720
|
+
let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0, sumYY = 0;
|
|
721
|
+
for (let i = 0; i < n; i++) {
|
|
722
|
+
sumX += xValues[i];
|
|
723
|
+
sumY += yValues[i];
|
|
724
|
+
sumXY += xValues[i] * yValues[i];
|
|
725
|
+
sumXX += xValues[i] * xValues[i];
|
|
726
|
+
sumYY += yValues[i] * yValues[i];
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
|
|
730
|
+
return { slope, n };
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
// This block is dynamically generated. Do not edit manually.
|
|
736
|
+
module.exports = {
|
|
737
|
+
Aggregators,
|
|
738
|
+
DataExtractor,
|
|
739
|
+
DistributionAnalytics,
|
|
740
|
+
HistoryExtractor,
|
|
741
|
+
MathPrimitives,
|
|
742
|
+
SCHEMAS,
|
|
743
|
+
SignalPrimitives,
|
|
744
|
+
TimeSeries,
|
|
745
|
+
Validators,
|
|
746
|
+
priceExtractor
|
|
346
747
|
};
|
package/index.js
CHANGED
|
@@ -36,7 +36,7 @@ const { build: buildManifestFunc } = require('./functions/computation-system/hel
|
|
|
36
36
|
const computationSystem = { runComputationPass : require('./functions/computation-system/helpers/computation_pass_runner') .runComputationPass,
|
|
37
37
|
dataLoader : require('./functions/computation-system/utils/data_loader'),
|
|
38
38
|
computationUtils : require('./functions/computation-system/utils/utils'),
|
|
39
|
-
buildManifest : buildManifestFunc
|
|
39
|
+
buildManifest : buildManifestFunc
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
// API
|