aiden-shared-calculations-unified 1.0.77 → 1.0.79
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/calculations/core/holding-duration-per-asset.js +2 -1
- package/calculations/core/instrument-price-change-1d.js +111 -0
- package/calculations/core/platform-daily-bought-vs-sold-count.js +2 -1
- package/calculations/core/platform-daily-ownership-delta.js +2 -1
- package/calculations/gauss/cohort-capital-flow.js +68 -30
- package/calculations/gauss/cohort-definer.js +46 -9
- package/calculations/gauss/gauss-divergence-signal.js +6 -14
- package/calculations/gem/skilled-cohort-flow.js +2 -1
- package/calculations/gem/unskilled-cohort-flow.js +2 -1
- package/calculations/helix/winner-loser-flow.js +2 -1
- package/package.json +1 -1
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* This calculation now uses the 'history' data source, not 'portfolio'.
|
|
8
8
|
*/
|
|
9
|
-
const { loadInstrumentMappings } = require('
|
|
9
|
+
const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
|
|
10
|
+
|
|
10
11
|
|
|
11
12
|
class HoldingDurationPerAsset {
|
|
12
13
|
constructor() {
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Calculation (Pass 1 - Meta) for 1-day price change.
|
|
3
|
+
*
|
|
4
|
+
* This metric answers: "What is the 1-day percentage price change for
|
|
5
|
+
* every instrument, handling for market holidays/weekends?"
|
|
6
|
+
*
|
|
7
|
+
* It is a 'meta' calculation that runs once, loads all price data,
|
|
8
|
+
* and provides a reusable 1-day change signal for downstream passes
|
|
9
|
+
* like cohort-capital-flow.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
class InstrumentPriceChange1D {
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Defines the output schema for this calculation.
|
|
16
|
+
*/
|
|
17
|
+
static getSchema() {
|
|
18
|
+
const tickerSchema = {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"properties": {
|
|
21
|
+
"price_change_1d_pct": {
|
|
22
|
+
"type": ["number", "null"],
|
|
23
|
+
"description": "The 1-day (business day adjusted) price change percentage."
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"required": ["price_change_1d_pct"]
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"description": "Calculates the 1-day price change for all instruments.",
|
|
32
|
+
"patternProperties": {
|
|
33
|
+
"^.*$": tickerSchema // Ticker
|
|
34
|
+
},
|
|
35
|
+
"additionalProperties": tickerSchema
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Statically defines all metadata for the manifest builder.
|
|
41
|
+
*/
|
|
42
|
+
static getMetadata() {
|
|
43
|
+
return {
|
|
44
|
+
type: 'meta',
|
|
45
|
+
rootDataDependencies: [], // Relies on price data, not root data
|
|
46
|
+
isHistorical: false, // It needs to look back 1 day, but is not 'historical' in the runner's sense
|
|
47
|
+
userType: 'n/a',
|
|
48
|
+
category: 'core_metrics' // Fits with other price/metric calcs
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* This is a Pass 1 calculation and has no dependencies.
|
|
54
|
+
*/
|
|
55
|
+
static getDependencies() {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Helper to get date string N days ago
|
|
60
|
+
_getDateStr(baseDateStr, daysOffset) {
|
|
61
|
+
const date = new Date(baseDateStr + 'T00:00:00Z');
|
|
62
|
+
date.setUTCDate(date.getUTCDate() + daysOffset);
|
|
63
|
+
return date.toISOString().slice(0, 10);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* This is a 'meta' calculation. It runs once.
|
|
68
|
+
* @param {string} dateStr - The date string 'YYYY-MM-DD'.
|
|
69
|
+
* @param {object} dependencies - The shared dependencies (e.g., logger, calculationUtils).
|
|
70
|
+
* @param {object} config - The computation system configuration.
|
|
71
|
+
* @param {object} fetchedDependencies - (Unused)
|
|
72
|
+
* @returns {Promise<object>} The calculation result.
|
|
73
|
+
*/
|
|
74
|
+
async process(dateStr, dependencies, config, fetchedDependencies) {
|
|
75
|
+
const { logger, calculationUtils } = dependencies;
|
|
76
|
+
|
|
77
|
+
// calculationUtils contains all exported functions from the /utils folder
|
|
78
|
+
const priceMap = await calculationUtils.loadAllPriceData();
|
|
79
|
+
const tickerMap = await calculationUtils.loadInstrumentMappings();
|
|
80
|
+
|
|
81
|
+
if (!priceMap || !tickerMap || !tickerMap.instrumentToTicker) {
|
|
82
|
+
logger.log('ERROR', '[instrument-price-change-1d] Failed to load priceMap or mappings.');
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const yesterdayStr = this._getDateStr(dateStr, -1);
|
|
87
|
+
const result = {};
|
|
88
|
+
|
|
89
|
+
for (const instrumentId in priceMap) {
|
|
90
|
+
const ticker = tickerMap.instrumentToTicker[instrumentId];
|
|
91
|
+
if (!ticker) continue;
|
|
92
|
+
|
|
93
|
+
// Use the utility function from price_data_provider.js
|
|
94
|
+
const priceChangeDecimal = calculationUtils.getDailyPriceChange(
|
|
95
|
+
instrumentId,
|
|
96
|
+
yesterdayStr,
|
|
97
|
+
dateStr,
|
|
98
|
+
priceMap
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
result[ticker] = {
|
|
102
|
+
// Convert decimal (0.05) to percentage (5.0) for consistency
|
|
103
|
+
price_change_1d_pct: priceChangeDecimal !== null ? priceChangeDecimal * 100 : null
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = InstrumentPriceChange1D;
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* This is different from 'daily_asset_activity' because it counts
|
|
8
8
|
* *positions*, not *unique users*.
|
|
9
9
|
*/
|
|
10
|
-
const { loadInstrumentMappings } = require('
|
|
10
|
+
const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
|
|
11
|
+
|
|
11
12
|
|
|
12
13
|
class DailyBoughtVsSoldCount {
|
|
13
14
|
constructor() {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* It runs ONCE, loads today's and yesterday's pre-aggregated 'insights' docs,
|
|
9
9
|
* and calculates the delta based on the 'total' field.
|
|
10
10
|
*/
|
|
11
|
-
const { loadInstrumentMappings } = require('
|
|
11
|
+
const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
|
|
12
|
+
|
|
12
13
|
|
|
13
14
|
class DailyOwnershipDelta {
|
|
14
15
|
|
|
@@ -6,24 +6,35 @@
|
|
|
6
6
|
* the price-adjusted capital flow for each cohort, per asset.
|
|
7
7
|
*
|
|
8
8
|
* This is the primary input for the final Pass 4 signal.
|
|
9
|
+
*
|
|
10
|
+
* --- REVISED 11/12/2025 ---
|
|
11
|
+
* - Removed dependency on 'insights' data.
|
|
12
|
+
* - Added dependency on 'instrument-price-change-1d' (Pass 1 meta calc).
|
|
13
|
+
* - Price adjustment logic now uses the reliable 1-day price change
|
|
14
|
+
* from the new dependency.
|
|
15
|
+
* --------------------------
|
|
9
16
|
*/
|
|
10
17
|
const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
|
|
11
18
|
|
|
12
19
|
class CohortCapitalFlow {
|
|
13
20
|
constructor() {
|
|
14
|
-
//
|
|
21
|
+
// { [cohortName]: Map<instrumentId, { flow_data... }> }
|
|
15
22
|
this.cohortFlows = new Map();
|
|
16
|
-
//
|
|
23
|
+
// { [userId]: "cohortName" }
|
|
17
24
|
this.cohortMap = new Map();
|
|
18
25
|
this.mappings = null;
|
|
19
26
|
this.dependenciesLoaded = false;
|
|
27
|
+
|
|
28
|
+
// --- NEW ---
|
|
29
|
+
// This will store the { [ticker]: { price_change_1d_pct: 5.5 } } map
|
|
30
|
+
this.priceChangeMap = null;
|
|
20
31
|
}
|
|
21
32
|
|
|
22
33
|
/**
|
|
23
34
|
* Defines the output schema for this calculation.
|
|
24
|
-
* @returns {object} JSON Schema object
|
|
25
35
|
*/
|
|
26
36
|
static getSchema() {
|
|
37
|
+
// ... (Schema remains unchanged) ...
|
|
27
38
|
const flowSchema = {
|
|
28
39
|
"type": "object",
|
|
29
40
|
"properties": {
|
|
@@ -56,7 +67,9 @@ class CohortCapitalFlow {
|
|
|
56
67
|
static getMetadata() {
|
|
57
68
|
return {
|
|
58
69
|
type: 'standard',
|
|
59
|
-
|
|
70
|
+
// --- REVISED ---
|
|
71
|
+
// Removed 'insights' as it's no longer needed for price.
|
|
72
|
+
rootDataDependencies: ['portfolio', 'history'],
|
|
60
73
|
isHistorical: true, // Needs T-1 portfolio for flow
|
|
61
74
|
userType: 'all',
|
|
62
75
|
category: 'gauss'
|
|
@@ -68,12 +81,13 @@ class CohortCapitalFlow {
|
|
|
68
81
|
*/
|
|
69
82
|
static getDependencies() {
|
|
70
83
|
return [
|
|
71
|
-
'cohort-definer' // from gauss (Pass 2)
|
|
84
|
+
'cohort-definer', // from gauss (Pass 2)
|
|
85
|
+
// --- REVISED ---
|
|
86
|
+
'instrument-price-change-1d' // from core (Pass 1)
|
|
72
87
|
];
|
|
73
88
|
}
|
|
74
89
|
|
|
75
90
|
_getPortfolioPositions(portfolio) {
|
|
76
|
-
// We MUST use AggregatedPositions for this to get 'Invested' (portfolio percentage)
|
|
77
91
|
return portfolio?.AggregatedPositions;
|
|
78
92
|
}
|
|
79
93
|
|
|
@@ -85,7 +99,7 @@ class CohortCapitalFlow {
|
|
|
85
99
|
this.cohortFlows.get(cohortName).set(instrumentId, {
|
|
86
100
|
total_invested_yesterday: 0,
|
|
87
101
|
total_invested_today: 0,
|
|
88
|
-
price_change_yesterday: 0, //
|
|
102
|
+
price_change_yesterday: 0, // Weighted sum
|
|
89
103
|
});
|
|
90
104
|
}
|
|
91
105
|
}
|
|
@@ -96,14 +110,22 @@ class CohortCapitalFlow {
|
|
|
96
110
|
_loadDependencies(fetchedDependencies) {
|
|
97
111
|
if (this.dependenciesLoaded) return;
|
|
98
112
|
|
|
113
|
+
// 1. Load Cohort Definitions
|
|
99
114
|
const cohortData = fetchedDependencies['cohort-definer'];
|
|
100
115
|
if (cohortData) {
|
|
101
116
|
for (const [cohortName, userIds] of Object.entries(cohortData)) {
|
|
102
|
-
|
|
103
|
-
|
|
117
|
+
if (Array.isArray(userIds)) {
|
|
118
|
+
for (const userId of userIds) {
|
|
119
|
+
this.cohortMap.set(userId, cohortName);
|
|
120
|
+
}
|
|
104
121
|
}
|
|
105
122
|
}
|
|
106
123
|
}
|
|
124
|
+
|
|
125
|
+
// 2. Load Price Change Data
|
|
126
|
+
// --- REVISED ---
|
|
127
|
+
this.priceChangeMap = fetchedDependencies['instrument-price-change-1d'] || {};
|
|
128
|
+
|
|
107
129
|
this.dependenciesLoaded = true;
|
|
108
130
|
}
|
|
109
131
|
|
|
@@ -116,7 +138,7 @@ class CohortCapitalFlow {
|
|
|
116
138
|
|
|
117
139
|
const cohortName = this.cohortMap.get(userId);
|
|
118
140
|
if (!cohortName) {
|
|
119
|
-
return; //
|
|
141
|
+
return; // Not in a defined cohort, skip.
|
|
120
142
|
}
|
|
121
143
|
|
|
122
144
|
if (!todayPortfolio || !yesterdayPortfolio) {
|
|
@@ -126,9 +148,14 @@ class CohortCapitalFlow {
|
|
|
126
148
|
const yPos = this._getPortfolioPositions(yesterdayPortfolio);
|
|
127
149
|
const tPos = this._getPortfolioPositions(todayPortfolio);
|
|
128
150
|
|
|
129
|
-
// We must have AggregatedPositions for both days to do this calculation
|
|
130
151
|
if (!yPos || !tPos) {
|
|
131
|
-
return;
|
|
152
|
+
return; // Must have AggregatedPositions for both days
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// --- REVISED ---
|
|
156
|
+
// We no longer need insightsMap
|
|
157
|
+
if (!this.priceChangeMap) {
|
|
158
|
+
return; // Cannot calculate price-adjusted flow
|
|
132
159
|
}
|
|
133
160
|
|
|
134
161
|
const yPosMap = new Map(yPos.map(p => [p.InstrumentID, p]));
|
|
@@ -144,15 +171,23 @@ class CohortCapitalFlow {
|
|
|
144
171
|
const yP = yPosMap.get(instrumentId);
|
|
145
172
|
const tP = tPosMap.get(instrumentId);
|
|
146
173
|
|
|
147
|
-
// 'Invested' is the portfolio percentage (e.g., 5.0 = 5%)
|
|
148
174
|
const yInvested = yP?.Invested || 0;
|
|
149
175
|
const tInvested = tP?.Invested || 0;
|
|
150
176
|
|
|
151
177
|
if (yInvested > 0) {
|
|
152
178
|
asset.total_invested_yesterday += yInvested;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
179
|
+
|
|
180
|
+
// --- REVISED ---
|
|
181
|
+
// Get the 1-day price change from our new dependency
|
|
182
|
+
const ticker = this.mappings.instrumentToTicker[instrumentId];
|
|
183
|
+
const yPriceChange_pct = (ticker && this.priceChangeMap[ticker])
|
|
184
|
+
? this.priceChangeMap[ticker].price_change_1d_pct
|
|
185
|
+
: 0;
|
|
186
|
+
|
|
187
|
+
// Convert from percentage (5.5) to decimal (0.055)
|
|
188
|
+
const yPriceChange_decimal = (yPriceChange_pct || 0) / 100.0;
|
|
189
|
+
|
|
190
|
+
asset.price_change_yesterday += yPriceChange_decimal * yInvested; // Weighted sum
|
|
156
191
|
}
|
|
157
192
|
if (tInvested > 0) {
|
|
158
193
|
asset.total_invested_today += tInvested;
|
|
@@ -168,6 +203,7 @@ class CohortCapitalFlow {
|
|
|
168
203
|
const finalResult = {};
|
|
169
204
|
|
|
170
205
|
for (const [cohortName, assetMap] of this.cohortFlows.entries()) {
|
|
206
|
+
// --- REVISED: Initialize cohortAssets as an object ---
|
|
171
207
|
const cohortAssets = {};
|
|
172
208
|
for (const [instrumentId, data] of assetMap.entries()) {
|
|
173
209
|
const ticker = this.mappings.instrumentToTicker[instrumentId];
|
|
@@ -176,32 +212,32 @@ class CohortCapitalFlow {
|
|
|
176
212
|
const { total_invested_yesterday, total_invested_today, price_change_yesterday } = data;
|
|
177
213
|
|
|
178
214
|
if (total_invested_yesterday > 0) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const avg_price_change_pct = price_change_yesterday / total_invested_yesterday;
|
|
182
|
-
|
|
183
|
-
// 2. Estimate yesterday's value *after* price change
|
|
184
|
-
// (This is what the value *would be* if no one bought or sold)
|
|
185
|
-
const price_adjusted_yesterday_value = total_invested_yesterday * (1 + avg_price_change_pct);
|
|
186
|
-
|
|
187
|
-
// 3. The difference between today's value and the price-adjusted
|
|
188
|
-
// value is the *net capital flow*.
|
|
215
|
+
const avg_price_change_decimal = price_change_yesterday / total_invested_yesterday;
|
|
216
|
+
const price_adjusted_yesterday_value = total_invested_yesterday * (1 + avg_price_change_decimal);
|
|
189
217
|
const flow_contribution = total_invested_today - price_adjusted_yesterday_value;
|
|
190
|
-
|
|
191
|
-
// 4. Normalize the flow as a percentage of yesterday's capital
|
|
192
218
|
const net_flow_percentage = (flow_contribution / total_invested_yesterday) * 100;
|
|
193
219
|
|
|
194
220
|
if (isFinite(net_flow_percentage) && isFinite(flow_contribution)) {
|
|
195
221
|
cohortAssets[ticker] = {
|
|
196
222
|
net_flow_percentage: net_flow_percentage,
|
|
197
|
-
net_flow_contribution: flow_contribution
|
|
223
|
+
net_flow_contribution: flow_contribution
|
|
198
224
|
};
|
|
199
225
|
}
|
|
226
|
+
} else if (total_invested_today > 0) {
|
|
227
|
+
cohortAssets[ticker] = {
|
|
228
|
+
net_flow_percentage: Infinity, // Represents pure inflow
|
|
229
|
+
net_flow_contribution: total_invested_today
|
|
230
|
+
};
|
|
200
231
|
}
|
|
201
232
|
}
|
|
233
|
+
// --- REVISED: Match schema { "cohortName": { "assets": { ... } } } ---
|
|
234
|
+
// This was a bug in your original file. The schema expected an object
|
|
235
|
+
// with an 'assets' key, but the code was returning the map directly.
|
|
202
236
|
finalResult[cohortName] = { assets: cohortAssets };
|
|
203
237
|
}
|
|
204
|
-
|
|
238
|
+
|
|
239
|
+
// --- REVISED: The schema for this calc shows the output is NOT sharded. ---
|
|
240
|
+
// The return should be the final object.
|
|
205
241
|
return finalResult;
|
|
206
242
|
}
|
|
207
243
|
|
|
@@ -210,6 +246,8 @@ class CohortCapitalFlow {
|
|
|
210
246
|
this.cohortMap.clear();
|
|
211
247
|
this.mappings = null;
|
|
212
248
|
this.dependenciesLoaded = false;
|
|
249
|
+
// --- NEW ---
|
|
250
|
+
this.priceChangeMap = null;
|
|
213
251
|
}
|
|
214
252
|
}
|
|
215
253
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* It then buckets these users into our final, named sub-cohorts
|
|
9
9
|
* (e.g., "Smart Investors", "FOMO Chasers").
|
|
10
10
|
*/
|
|
11
|
-
const { loadInstrumentMappings } = require('
|
|
11
|
+
const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
|
|
12
12
|
|
|
13
13
|
class CohortDefiner {
|
|
14
14
|
constructor() {
|
|
@@ -58,7 +58,10 @@ class CohortDefiner {
|
|
|
58
58
|
"smart_scalpers": cohortSchema,
|
|
59
59
|
"fomo_chasers": cohortSchema,
|
|
60
60
|
"patient_losers": cohortSchema,
|
|
61
|
-
"fomo_bagholders": cohortSchema
|
|
61
|
+
"fomo_bagholders": cohortSchema,
|
|
62
|
+
// --- FIX [PROBLEM 9]: Add uncategorized bucket ---
|
|
63
|
+
"uncategorized_smart": cohortSchema,
|
|
64
|
+
"uncategorized_dumb": cohortSchema
|
|
62
65
|
},
|
|
63
66
|
"additionalProperties": cohortSchema
|
|
64
67
|
};
|
|
@@ -73,14 +76,29 @@ class CohortDefiner {
|
|
|
73
76
|
const dnaFilterData = fetchedDependencies['daily-dna-filter'];
|
|
74
77
|
this.momentumData = fetchedDependencies['instrument-price-momentum-20d'];
|
|
75
78
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
// --- FIX [PROBLEM 6]: Validate dependency content, not just existence ---
|
|
80
|
+
if (dnaFilterData && dnaFilterData.smart_cohort_ids && dnaFilterData.dumb_cohort_ids) {
|
|
81
|
+
this.cohortIdSets = {
|
|
82
|
+
smart: new Set(dnaFilterData.smart_cohort_ids),
|
|
83
|
+
dumb: new Set(dnaFilterData.dumb_cohort_ids)
|
|
84
|
+
};
|
|
85
|
+
} else {
|
|
86
|
+
// Initialize with empty sets if dependency is missing or malformed
|
|
87
|
+
this.cohortIdSets = {
|
|
88
|
+
smart: new Set(),
|
|
89
|
+
dumb: new Set()
|
|
90
|
+
};
|
|
91
|
+
}
|
|
80
92
|
}
|
|
81
93
|
|
|
82
94
|
_getFomoScore(todayPortfolio, yesterdayPortfolio) {
|
|
83
95
|
if (!this.mappings) return 0;
|
|
96
|
+
|
|
97
|
+
// --- FIX [PROBLEM 4 related]: Ensure momentum data is loaded ---
|
|
98
|
+
if (!this.momentumData) {
|
|
99
|
+
return 0; // Cannot calculate FOMO without momentum data
|
|
100
|
+
}
|
|
101
|
+
|
|
84
102
|
const yIds = new Set((yesterdayPortfolio?.AggregatedPositions || []).map(p => p.InstrumentID));
|
|
85
103
|
const newPositions = (todayPortfolio?.AggregatedPositions || []).filter(p => p.InstrumentID && !yIds.has(p.InstrumentID));
|
|
86
104
|
if (newPositions.length === 0) return 0;
|
|
@@ -89,6 +107,7 @@ class CohortDefiner {
|
|
|
89
107
|
let count = 0;
|
|
90
108
|
for (const pos of newPositions) {
|
|
91
109
|
const ticker = this.mappings.instrumentToTicker[pos.InstrumentID];
|
|
110
|
+
// --- FIX [PROBLEM 4 related]: Check momentumData[ticker] exists ---
|
|
92
111
|
if (ticker && this.momentumData[ticker]) {
|
|
93
112
|
fomoSum += this.momentumData[ticker].momentum_20d_pct || 0;
|
|
94
113
|
count++;
|
|
@@ -165,19 +184,32 @@ class CohortDefiner {
|
|
|
165
184
|
if (vectors.length === 0) return 0;
|
|
166
185
|
const sorted = vectors.map(v => v[key]).sort((a, b) => a - b);
|
|
167
186
|
const mid = Math.floor(sorted.length / 2);
|
|
187
|
+
// Handle even-length array by taking average of middle two
|
|
188
|
+
if (sorted.length % 2 === 0 && sorted.length > 0) {
|
|
189
|
+
return (sorted[mid - 1] + sorted[mid]) / 2;
|
|
190
|
+
}
|
|
168
191
|
return sorted[mid];
|
|
169
192
|
}
|
|
170
193
|
|
|
171
194
|
getResult() {
|
|
172
195
|
const cohorts = {};
|
|
196
|
+
const assignedSmart = new Set();
|
|
197
|
+
const assignedDumb = new Set();
|
|
173
198
|
|
|
174
199
|
// 1. Process Smart Cohort
|
|
175
200
|
const smart_median_time = this._getMedian(this.smartVectors, 'time');
|
|
201
|
+
|
|
176
202
|
cohorts['smart_investors'] = this.smartVectors
|
|
177
203
|
.filter(u => u.time >= smart_median_time)
|
|
178
|
-
.map(u => u.userId);
|
|
204
|
+
.map(u => { assignedSmart.add(u.userId); return u.userId; });
|
|
205
|
+
|
|
179
206
|
cohorts['smart_scalpers'] = this.smartVectors
|
|
180
207
|
.filter(u => u.time < smart_median_time)
|
|
208
|
+
.map(u => { assignedSmart.add(u.userId); return u.userId; });
|
|
209
|
+
|
|
210
|
+
// --- FIX [PROBLEM 9]: Add uncategorized bucket ---
|
|
211
|
+
cohorts['uncategorized_smart'] = this.smartVectors
|
|
212
|
+
.filter(u => !assignedSmart.has(u.userId))
|
|
181
213
|
.map(u => u.userId);
|
|
182
214
|
|
|
183
215
|
// 2. Process Dumb Cohort
|
|
@@ -186,14 +218,19 @@ class CohortDefiner {
|
|
|
186
218
|
|
|
187
219
|
cohorts['fomo_chasers'] = this.dumbVectors
|
|
188
220
|
.filter(u => u.fomo >= dumb_median_fomo && u.bagholder < dumb_median_bag)
|
|
189
|
-
.map(u => u.userId);
|
|
221
|
+
.map(u => { assignedDumb.add(u.userId); return u.userId; });
|
|
190
222
|
|
|
191
223
|
cohorts['patient_losers'] = this.dumbVectors
|
|
192
224
|
.filter(u => u.fomo < dumb_median_fomo && u.bagholder >= dumb_median_bag)
|
|
193
|
-
.map(u => u.userId);
|
|
225
|
+
.map(u => { assignedDumb.add(u.userId); return u.userId; });
|
|
194
226
|
|
|
195
227
|
cohorts['fomo_bagholders'] = this.dumbVectors
|
|
196
228
|
.filter(u => u.fomo >= dumb_median_fomo && u.bagholder >= dumb_median_bag)
|
|
229
|
+
.map(u => { assignedDumb.add(u.userId); return u.userId; });
|
|
230
|
+
|
|
231
|
+
// --- FIX [PROBLEM 9]: Add uncategorized bucket ---
|
|
232
|
+
cohorts['uncategorized_dumb'] = this.dumbVectors
|
|
233
|
+
.filter(u => !assignedDumb.has(u.userId))
|
|
197
234
|
.map(u => u.userId);
|
|
198
235
|
|
|
199
236
|
// Output is a compact map of cohort_name -> [userIds]
|
|
@@ -94,20 +94,20 @@ class GaussDivergenceSignal {
|
|
|
94
94
|
return {};
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
// Define which cohorts are "Smart" and which are "Dumb"
|
|
98
|
-
// These names must match the keys from Pass 2
|
|
99
97
|
const SMART_COHORTS = ['smart_investors', 'smart_scalpers'];
|
|
100
98
|
const DUMB_COHORTS = ['fomo_chasers', 'patient_losers', 'fomo_bagholders'];
|
|
101
99
|
|
|
102
100
|
const blendedFlows = new Map(); // Map<ticker, { smart: 0, dumb: 0 }>
|
|
103
101
|
|
|
104
|
-
// 1. Blend all cohort flows
|
|
102
|
+
// 1. Blend all cohort flows
|
|
105
103
|
for (const cohortName in cohortFlows) {
|
|
106
104
|
const isSmart = SMART_COHORTS.includes(cohortName);
|
|
107
105
|
const isDumb = DUMB_COHORTS.includes(cohortName);
|
|
108
106
|
if (!isSmart && !isDumb) continue;
|
|
109
107
|
|
|
110
|
-
|
|
108
|
+
// --- REVISED ---
|
|
109
|
+
// Read from the 'assets' property, which contains the map of tickers
|
|
110
|
+
const assets = cohortFlows[cohortName]?.assets;
|
|
111
111
|
if (!assets) continue;
|
|
112
112
|
|
|
113
113
|
for (const [ticker, data] of Object.entries(assets)) {
|
|
@@ -115,7 +115,6 @@ class GaussDivergenceSignal {
|
|
|
115
115
|
blendedFlows.set(ticker, { smart: 0, dumb: 0 });
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
// Use net_flow_contribution, which is the %-point flow
|
|
119
118
|
const flow = data.net_flow_contribution || 0;
|
|
120
119
|
|
|
121
120
|
if (isSmart) {
|
|
@@ -126,20 +125,14 @@ class GaussDivergenceSignal {
|
|
|
126
125
|
}
|
|
127
126
|
}
|
|
128
127
|
|
|
129
|
-
// 2. Calculate final signal
|
|
128
|
+
// 2. Calculate final signal (logic unchanged)
|
|
130
129
|
const result = {};
|
|
131
130
|
for (const [ticker, data] of blendedFlows.entries()) {
|
|
132
|
-
|
|
133
|
-
// The core signal is the divergence: (Smart Flow - Dumb Flow)
|
|
134
|
-
// If Smart buys (+1) and Dumb sells (-1), score is +2.
|
|
135
|
-
// If Smart sells (-1) and Dumb buys (+1), score is -2.
|
|
136
131
|
const divergence = data.smart - data.dumb;
|
|
137
|
-
|
|
138
|
-
// Normalize the score to a -10 to +10 range
|
|
139
132
|
const gauss_score = this._normalize(divergence);
|
|
140
133
|
|
|
141
134
|
let signal = "Neutral";
|
|
142
|
-
if (gauss_score > 7.0) signal = "Strong Buy";
|
|
135
|
+
if (gauss_score > 7.0) signal = "Strong Buy";
|
|
143
136
|
else if (gauss_score > 2.0) signal = "Buy";
|
|
144
137
|
else if (gauss_score < -7.0) signal = "Strong Sell";
|
|
145
138
|
else if (gauss_score < -2.0) signal = "Sell";
|
|
@@ -152,7 +145,6 @@ class GaussDivergenceSignal {
|
|
|
152
145
|
};
|
|
153
146
|
}
|
|
154
147
|
|
|
155
|
-
// Final output is compact and non-sharded.
|
|
156
148
|
return result;
|
|
157
149
|
}
|
|
158
150
|
}
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* This calculation *depends* on 'cohort-skill-definition'
|
|
8
8
|
* to identify the cohort.
|
|
9
9
|
*/
|
|
10
|
-
const { loadInstrumentMappings } = require('
|
|
10
|
+
const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
|
|
11
|
+
|
|
11
12
|
|
|
12
13
|
class SkilledCohortFlow {
|
|
13
14
|
constructor() {
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* This calculation *depends* on 'cohort-skill-definition'
|
|
8
8
|
* to identify the cohort.
|
|
9
9
|
*/
|
|
10
|
-
const { loadInstrumentMappings } = require('
|
|
10
|
+
const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
|
|
11
|
+
|
|
11
12
|
|
|
12
13
|
class UnskilledCohortFlow {
|
|
13
14
|
constructor() {
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* This is a 'standard' calc because it must iterate over all
|
|
12
12
|
* users to compare their T-1 vs T portfolios.
|
|
13
13
|
*/
|
|
14
|
-
const { loadInstrumentMappings } = require('
|
|
14
|
+
const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
|
|
15
|
+
|
|
15
16
|
|
|
16
17
|
class WinnerLoserFlow {
|
|
17
18
|
constructor() {
|