aiden-shared-calculations-unified 1.0.82 → 1.0.84
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/asset-pnl-status.js +122 -104
- package/calculations/core/asset-position-size.js +110 -73
- package/calculations/core/average-daily-pnl-all-users.js +17 -3
- package/calculations/core/average-daily-pnl-per-sector.js +83 -75
- package/calculations/core/average-daily-pnl-per-stock.js +84 -73
- package/calculations/core/average-daily-position-pnl.js +2 -2
- package/calculations/core/holding-duration-per-asset.js +24 -23
- package/calculations/core/instrument-price-change-1d.js +72 -82
- package/calculations/core/instrument-price-momentum-20d.js +66 -100
- package/calculations/core/long-position-per-stock.js +21 -13
- package/calculations/core/overall-holding-duration.js +8 -3
- package/calculations/core/overall-profitability-ratio.js +2 -2
- package/calculations/core/platform-buy-sell-sentiment.js +75 -22
- package/calculations/core/platform-daily-bought-vs-sold-count.js +19 -10
- package/calculations/core/platform-daily-ownership-delta.js +39 -15
- package/calculations/core/platform-ownership-per-sector.js +38 -18
- package/calculations/core/platform-total-positions-held.js +36 -14
- package/calculations/core/pnl-distribution-per-stock.js +39 -36
- package/calculations/core/price-metrics.js +70 -172
- package/calculations/core/profitability-ratio-per-sector.js +23 -29
- package/calculations/core/profitability-ratio-per-stock.js +20 -13
- package/calculations/core/profitability-skew-per-stock.js +20 -13
- package/calculations/core/profitable-and-unprofitable-status.js +34 -10
- package/calculations/core/sentiment-per-stock.js +20 -9
- package/calculations/core/short-position-per-stock.js +23 -37
- package/calculations/core/social-activity-aggregation.js +41 -115
- package/calculations/core/social-asset-posts-trend.js +77 -94
- package/calculations/core/social-event-correlation.js +87 -106
- package/calculations/core/social-sentiment-aggregation.js +56 -138
- package/calculations/core/social-top-mentioned-words.js +74 -106
- package/calculations/core/social-topic-interest-evolution.js +94 -94
- package/calculations/core/social-topic-sentiment-matrix.js +90 -74
- package/calculations/core/social-word-mentions-trend.js +92 -106
- package/calculations/core/speculator-asset-sentiment.js +63 -92
- package/calculations/core/speculator-danger-zone.js +77 -90
- package/calculations/core/speculator-distance-to-stop-loss-per-leverage.js +75 -90
- package/calculations/core/speculator-distance-to-tp-per-leverage.js +75 -88
- package/calculations/core/speculator-entry-distance-to-sl-per-leverage.js +75 -90
- package/calculations/core/speculator-entry-distance-to-tp-per-leverage.js +74 -89
- package/calculations/core/speculator-leverage-per-asset.js +62 -57
- package/calculations/core/speculator-leverage-per-sector.js +53 -65
- package/calculations/core/speculator-risk-reward-ratio-per-asset.js +71 -76
- package/calculations/core/speculator-stop-loss-distance-by-sector-short-long-breakdown.js +60 -81
- package/calculations/core/speculator-stop-loss-distance-by-ticker-short-long-breakdown.js +57 -77
- package/calculations/core/speculator-stop-loss-per-asset.js +43 -80
- package/calculations/core/speculator-take-profit-per-asset.js +45 -69
- package/calculations/core/speculator-tsl-per-asset.js +42 -49
- package/calculations/core/total-long-figures.js +19 -19
- package/calculations/core/total-long-per-sector.js +39 -36
- package/calculations/core/total-short-figures.js +19 -19
- package/calculations/core/total-short-per-sector.js +39 -36
- package/calculations/core/users-processed.js +52 -25
- package/calculations/gauss/cohort-capital-flow.js +38 -29
- package/calculations/gauss/cohort-definer.js +17 -25
- package/calculations/gauss/daily-dna-filter.js +10 -4
- package/calculations/gauss/gauss-divergence-signal.js +28 -6
- package/calculations/gem/cohort-momentum-state.js +113 -92
- package/calculations/gem/cohort-skill-definition.js +23 -53
- package/calculations/gem/platform-conviction-divergence.js +62 -116
- package/calculations/gem/quant-skill-alpha-signal.js +107 -123
- package/calculations/gem/skilled-cohort-flow.js +178 -167
- package/calculations/gem/skilled-unskilled-divergence.js +73 -113
- package/calculations/gem/unskilled-cohort-flow.js +176 -166
- package/calculations/helix/helix-contrarian-signal.js +91 -83
- package/calculations/helix/herd-consensus-score.js +135 -97
- package/calculations/helix/winner-loser-flow.js +14 -14
- package/calculations/pyro/risk-appetite-index.js +121 -123
- package/calculations/pyro/squeeze-potential.js +93 -125
- package/calculations/pyro/volatility-signal.js +109 -97
- package/package.json +9 -9
- package/README.MD +0 -78
|
@@ -1,240 +1,250 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* to
|
|
2
|
+
* @fileoverview GEM Product Line (Pass 2)
|
|
3
|
+
* --- FIX ---
|
|
4
|
+
* - Added defensive checks in '_loadDependencies' to prevent crash
|
|
5
|
+
* when dependencies fail to load (e.g., due to worker bug).
|
|
6
|
+
* - **FIXED:** Restored logic to calculate 'avg_position_change_pct'
|
|
7
|
+
* to resolve the hardcoded '0' value. This involves
|
|
8
|
+
* re-adding fields to '_initFlowData' and 'process'.
|
|
9
9
|
*/
|
|
10
|
-
const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
|
|
11
|
-
|
|
12
10
|
|
|
13
11
|
class UnskilledCohortFlow {
|
|
14
12
|
constructor() {
|
|
15
|
-
this.
|
|
16
|
-
this.
|
|
17
|
-
this.
|
|
18
|
-
this.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Defines the output schema for this calculation.
|
|
23
|
-
* @returns {object} JSON Schema object
|
|
24
|
-
*/
|
|
25
|
-
static getSchema() {
|
|
26
|
-
const flowSchema = {
|
|
27
|
-
"type": "object",
|
|
28
|
-
"properties": {
|
|
29
|
-
"net_flow_percentage": { "type": "number" },
|
|
30
|
-
"total_invested_today": { "type": "number" },
|
|
31
|
-
"total_invested_yesterday": { "type": "number" }
|
|
32
|
-
},
|
|
33
|
-
"required": ["net_flow_percentage", "total_invested_today", "total_invested_yesterday"]
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
"type": "object",
|
|
38
|
-
"description": "Calculates net capital flow % (price-adjusted) for the 'Unskilled Cohort' (bottom 20% by skill score), aggregated by asset and sector.",
|
|
39
|
-
"properties": {
|
|
40
|
-
"cohort_size": {
|
|
41
|
-
"type": "number",
|
|
42
|
-
"description": "The number of users identified as being in the Unskilled Cohort."
|
|
43
|
-
},
|
|
44
|
-
"assets": {
|
|
45
|
-
"type": "object",
|
|
46
|
-
"description": "Price-adjusted net flow per asset.",
|
|
47
|
-
"patternProperties": { "^.*$": flowSchema }, // Ticker
|
|
48
|
-
"additionalProperties": flowSchema
|
|
49
|
-
},
|
|
50
|
-
"sectors": {
|
|
51
|
-
"type": "object",
|
|
52
|
-
"description": "Price-adjusted net flow per sector.",
|
|
53
|
-
"patternProperties": { "^.*$": flowSchema }, // Sector
|
|
54
|
-
"additionalProperties": flowSchema
|
|
55
|
-
}
|
|
56
|
-
},
|
|
57
|
-
"required": ["cohort_size", "assets", "sectors"]
|
|
58
|
-
};
|
|
13
|
+
this.assetFlows = new Map();
|
|
14
|
+
this.cohortMap = new Map();
|
|
15
|
+
this.tickerMap = null;
|
|
16
|
+
this.dependenciesLoaded = false;
|
|
17
|
+
this.priceChangeMap = null;
|
|
59
18
|
}
|
|
60
19
|
|
|
61
|
-
/**
|
|
62
|
-
* Statically defines all metadata for the manifest builder.
|
|
63
|
-
*/
|
|
64
20
|
static getMetadata() {
|
|
65
21
|
return {
|
|
66
22
|
type: 'standard',
|
|
67
23
|
rootDataDependencies: ['portfolio'],
|
|
68
|
-
isHistorical: true,
|
|
24
|
+
isHistorical: true,
|
|
69
25
|
userType: 'all',
|
|
70
26
|
category: 'gem'
|
|
71
27
|
};
|
|
72
28
|
}
|
|
73
29
|
|
|
74
|
-
/**
|
|
75
|
-
* Statically declare dependencies.
|
|
76
|
-
*/
|
|
77
30
|
static getDependencies() {
|
|
78
|
-
return [
|
|
31
|
+
return [
|
|
32
|
+
'cohort-skill-definition', // from gem (Pass 1)
|
|
33
|
+
'instrument-price-change-1d' // from core (Pass 1)
|
|
34
|
+
];
|
|
79
35
|
}
|
|
80
36
|
|
|
81
|
-
|
|
82
|
-
|
|
37
|
+
static getSchema() {
|
|
38
|
+
const tickerSchema = {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"properties": {
|
|
41
|
+
"net_flow_pct": { "type": "number" },
|
|
42
|
+
"net_flow_contribution": { "type": "number" },
|
|
43
|
+
"avg_position_change_pct": { "type": "number" },
|
|
44
|
+
"user_count": { "type": "number" }
|
|
45
|
+
},
|
|
46
|
+
"required": ["net_flow_pct", "net_flow_contribution", "avg_position_change_pct", "user_count"]
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"description": "Calculates capital flow and conviction change for the 'Unskilled' cohort.",
|
|
52
|
+
"patternProperties": { "^.*$": tickerSchema },
|
|
53
|
+
"additionalProperties": tickerSchema
|
|
54
|
+
};
|
|
83
55
|
}
|
|
84
56
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
total_invested_yesterday: 0,
|
|
89
|
-
total_invested_today: 0,
|
|
90
|
-
price_change_yesterday: 0,
|
|
91
|
-
});
|
|
92
|
-
}
|
|
57
|
+
_getPortfolioPositions(portfolio) {
|
|
58
|
+
// --- FIX: Support both Normal (Aggregated) and Speculator (Public) ---
|
|
59
|
+
return portfolio?.AggregatedPositions || portfolio?.PublicPositions;
|
|
93
60
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
61
|
+
|
|
62
|
+
// --- THIS IS THE FIX (Part 1) ---
|
|
63
|
+
_initFlowData(instrumentId) {
|
|
64
|
+
if (!this.assetFlows.has(instrumentId)) {
|
|
65
|
+
this.assetFlows.set(instrumentId, {
|
|
98
66
|
total_invested_yesterday: 0,
|
|
99
67
|
total_invested_today: 0,
|
|
100
68
|
price_change_yesterday: 0,
|
|
69
|
+
// Restore fields needed for avg_position_change_pct
|
|
70
|
+
total_pos_size_yesterday: 0,
|
|
71
|
+
total_pos_size_today: 0,
|
|
72
|
+
user_count_yesterday: 0,
|
|
73
|
+
user_count_today: 0
|
|
101
74
|
});
|
|
102
75
|
}
|
|
103
76
|
}
|
|
77
|
+
// --- END FIX (Part 1) ---
|
|
104
78
|
|
|
105
|
-
|
|
106
|
-
if (this.
|
|
107
|
-
|
|
79
|
+
_loadDependencies(fetchedDependencies) {
|
|
80
|
+
if (this.dependenciesLoaded) return;
|
|
81
|
+
|
|
82
|
+
if (!fetchedDependencies) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`[unskilled-cohort-flow] CRITICAL ERROR: fetchedDependencies object was UNDEFINED.
|
|
85
|
+
This means a dependency test (like 'cohort-skill-definition' or 'instrument-price-change-1d')
|
|
86
|
+
failed to run or produced an invalid result.`
|
|
87
|
+
);
|
|
108
88
|
}
|
|
109
|
-
|
|
110
|
-
// FIX: Use normalized dependency name
|
|
89
|
+
|
|
111
90
|
const cohortData = fetchedDependencies['cohort-skill-definition'];
|
|
112
|
-
if (!cohortData
|
|
113
|
-
|
|
91
|
+
if (!cohortData) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`[unskilled-cohort-flow] DEPENDENCY ERROR: 'cohort-skill-definition' was missing.
|
|
94
|
+
Check logs for errors in that calculation.`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// This logic is correct for 'cohort-skill-definition' output
|
|
99
|
+
if (cohortData && cohortData.unskilled_user_ids) {
|
|
100
|
+
(cohortData.unskilled_user_ids || []).forEach(uid => this.cohortMap.set(String(uid), 'unskilled'));
|
|
114
101
|
}
|
|
115
102
|
|
|
116
|
-
this.
|
|
117
|
-
|
|
103
|
+
this.priceChangeMap = fetchedDependencies['instrument-price-change-1d'];
|
|
104
|
+
if (!this.priceChangeMap) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`[unskilled-cohort-flow] DEPENDENCY ERROR: 'instrument-price-change-1d' was missing.
|
|
107
|
+
This is likely due to the worker bug for 'price' dependencies.`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.dependenciesLoaded = true;
|
|
118
112
|
}
|
|
119
113
|
|
|
114
|
+
// --- THIS IS THE FIX (Part 2) ---
|
|
120
115
|
process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights, fetchedDependencies) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (!
|
|
124
|
-
|
|
116
|
+
this._loadDependencies(fetchedDependencies);
|
|
117
|
+
|
|
118
|
+
if (!this.tickerMap) {
|
|
119
|
+
this.tickerMap = context.instrumentToTicker;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const cohortName = this.cohortMap.get(String(userId));
|
|
123
|
+
if (cohortName !== 'unskilled') {
|
|
124
|
+
return; // Not in unskilled cohort
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
if (!todayPortfolio || !yesterdayPortfolio) {
|
|
127
|
+
if (!todayPortfolio || !yesterdayPortfolio || !this.priceChangeMap || !this.tickerMap) {
|
|
128
128
|
return;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
// Logic from here is identical to smart-cohort-flow.js
|
|
132
131
|
const yPos = this._getPortfolioPositions(yesterdayPortfolio);
|
|
133
132
|
const tPos = this._getPortfolioPositions(todayPortfolio);
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
if (!yPos || !tPos) return;
|
|
134
|
+
|
|
135
|
+
const yPosMap = new Map(yPos.map(p => [p.InstrumentID, p]));
|
|
136
|
+
const tPosMap = new Map(tPos.map(p => [p.InstrumentID, p]));
|
|
136
137
|
const allInstrumentIds = new Set([...yPosMap.keys(), ...tPosMap.keys()]);
|
|
137
|
-
|
|
138
|
-
if (!this.mappings) {
|
|
139
|
-
this.mappings = context.mappings;
|
|
140
|
-
}
|
|
141
138
|
|
|
142
139
|
for (const instrumentId of allInstrumentIds) {
|
|
143
140
|
if (!instrumentId) continue;
|
|
144
|
-
|
|
145
|
-
this.
|
|
146
|
-
const asset = this.
|
|
141
|
+
|
|
142
|
+
this._initFlowData(instrumentId);
|
|
143
|
+
const asset = this.assetFlows.get(instrumentId);
|
|
144
|
+
|
|
147
145
|
const yP = yPosMap.get(instrumentId);
|
|
148
146
|
const tP = tPosMap.get(instrumentId);
|
|
147
|
+
|
|
148
|
+
// Get $ flow
|
|
149
|
+
const yInvested = yP?.Invested || yP?.Amount || 0;
|
|
150
|
+
const tInvested = tP?.Invested || tP?.Amount || 0;
|
|
149
151
|
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
const sector = this.mappings.instrumentToSector[instrumentId] || 'Other';
|
|
154
|
-
this._initSector(sector);
|
|
155
|
-
const sectorAsset = this.sectorData.get(sector);
|
|
152
|
+
// Get position size (same as $ flow for these schemas)
|
|
153
|
+
const ySize = yP?.Invested || yP?.Amount || 0;
|
|
154
|
+
const tSize = tP?.Invested || tP?.Amount || 0;
|
|
156
155
|
|
|
157
156
|
if (yInvested > 0) {
|
|
158
|
-
const yPriceChange = (yP?.NetProfit || 0) / (yP?.Invested || 1);
|
|
159
|
-
|
|
160
157
|
asset.total_invested_yesterday += yInvested;
|
|
161
|
-
|
|
158
|
+
|
|
159
|
+
const ticker = this.tickerMap[instrumentId];
|
|
160
|
+
const yPriceChange_pct = (ticker && this.priceChangeMap[ticker])
|
|
161
|
+
? this.priceChangeMap[ticker].price_change_1d_pct
|
|
162
|
+
: 0;
|
|
163
|
+
|
|
164
|
+
const yPriceChange_decimal = (yPriceChange_pct || 0) / 100.0;
|
|
165
|
+
asset.price_change_yesterday += yPriceChange_decimal * yInvested;
|
|
162
166
|
|
|
163
|
-
|
|
164
|
-
|
|
167
|
+
// Track yesterday's size and user count
|
|
168
|
+
asset.total_pos_size_yesterday += ySize;
|
|
169
|
+
asset.user_count_yesterday++;
|
|
165
170
|
}
|
|
166
171
|
if (tInvested > 0) {
|
|
167
172
|
asset.total_invested_today += tInvested;
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
_calculateFlow(dataMap) {
|
|
174
|
-
// This helper is identical to the one in skilled-cohort-flow
|
|
175
|
-
const result = {};
|
|
176
|
-
for (const [key, data] of dataMap.entries()) {
|
|
177
|
-
const { total_invested_yesterday, total_invested_today, price_change_yesterday } = data;
|
|
178
|
-
|
|
179
|
-
if (total_invested_yesterday > 0) {
|
|
180
|
-
const avg_price_change_pct = price_change_yesterday / total_invested_yesterday;
|
|
181
|
-
const price_contribution = total_invested_yesterday * avg_price_change_pct;
|
|
182
|
-
const flow_contribution = total_invested_today - (total_invested_yesterday + price_contribution);
|
|
183
|
-
const net_flow_percentage = (flow_contribution / total_invested_yesterday) * 100;
|
|
173
|
+
asset.user_count_today++;
|
|
184
174
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
total_invested_today: total_invested_today,
|
|
188
|
-
total_invested_yesterday: total_invested_yesterday
|
|
189
|
-
};
|
|
175
|
+
// Track today's size
|
|
176
|
+
asset.total_pos_size_today += tSize;
|
|
190
177
|
}
|
|
191
178
|
}
|
|
192
|
-
return result;
|
|
193
179
|
}
|
|
180
|
+
// --- END FIX (Part 2) ---
|
|
194
181
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
182
|
+
// --- THIS IS THE FIX (Part 3) ---
|
|
183
|
+
async getResult() {
|
|
184
|
+
if (!this.tickerMap) {
|
|
185
|
+
return {};
|
|
198
186
|
}
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const {
|
|
207
|
-
|
|
187
|
+
|
|
188
|
+
const finalResult = {};
|
|
189
|
+
|
|
190
|
+
for (const [instrumentId, data] of this.assetFlows.entries()) {
|
|
191
|
+
const ticker = this.tickerMap[instrumentId];
|
|
192
|
+
if (!ticker) continue;
|
|
193
|
+
|
|
194
|
+
const {
|
|
195
|
+
total_invested_yesterday, total_invested_today, price_change_yesterday,
|
|
196
|
+
// Destructure the restored fields
|
|
197
|
+
total_pos_size_yesterday, total_pos_size_today,
|
|
198
|
+
user_count_yesterday, user_count_today
|
|
199
|
+
} = data;
|
|
200
|
+
|
|
201
|
+
let net_flow_percentage = 0;
|
|
202
|
+
let flow_contribution = 0;
|
|
203
|
+
|
|
208
204
|
if (total_invested_yesterday > 0) {
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
205
|
+
const avg_price_change_decimal = (price_change_yesterday === 0) ? 0 : (price_change_yesterday / total_invested_yesterday);
|
|
206
|
+
const price_adjusted_yesterday_value = total_invested_yesterday * (1 + avg_price_change_decimal);
|
|
207
|
+
|
|
208
|
+
flow_contribution = total_invested_today - price_adjusted_yesterday_value;
|
|
209
|
+
net_flow_percentage = (flow_contribution / total_invested_yesterday) * 100;
|
|
210
|
+
} else if (total_invested_today > 0) {
|
|
211
|
+
flow_contribution = total_invested_today;
|
|
212
|
+
net_flow_percentage = Infinity;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Restore the calculation logic for conviction
|
|
216
|
+
let avg_position_change_pct = 0;
|
|
217
|
+
if (user_count_yesterday > 0 && user_count_today > 0) {
|
|
218
|
+
const avg_pos_y = total_pos_size_yesterday / user_count_yesterday;
|
|
219
|
+
const avg_pos_t = total_pos_size_today / user_count_today;
|
|
220
|
+
if (avg_pos_y > 0) {
|
|
221
|
+
avg_position_change_pct = ((avg_pos_t - avg_pos_y) / avg_pos_y) * 100;
|
|
222
|
+
}
|
|
223
|
+
} else if (user_count_today > 0) {
|
|
224
|
+
// Pure inflow, conviction is max
|
|
225
|
+
avg_position_change_pct = Infinity;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (isFinite(net_flow_percentage) && isFinite(flow_contribution)) {
|
|
229
|
+
finalResult[ticker] = {
|
|
230
|
+
net_flow_pct: net_flow_percentage,
|
|
231
|
+
net_flow_contribution: flow_contribution,
|
|
232
|
+
avg_position_change_pct: avg_position_change_pct, // Now correctly calculated
|
|
233
|
+
user_count: user_count_today
|
|
218
234
|
};
|
|
219
235
|
}
|
|
220
236
|
}
|
|
221
|
-
|
|
222
|
-
// 2. Calculate Sector Flow
|
|
223
|
-
const sectorResult = this._calculateFlow(this.sectorData);
|
|
224
237
|
|
|
225
|
-
return
|
|
226
|
-
cohort_size: unskilledCohort.size,
|
|
227
|
-
assets: assetResult,
|
|
228
|
-
sectors: sectorResult
|
|
229
|
-
};
|
|
238
|
+
return finalResult;
|
|
230
239
|
}
|
|
240
|
+
// --- END FIX (Part 3) ---
|
|
231
241
|
|
|
232
242
|
reset() {
|
|
233
|
-
this.
|
|
234
|
-
this.
|
|
235
|
-
this.
|
|
236
|
-
this.
|
|
243
|
+
this.assetFlows.clear();
|
|
244
|
+
this.cohortMap.clear();
|
|
245
|
+
this.tickerMap = null;
|
|
246
|
+
this.dependenciesLoaded = false;
|
|
247
|
+
this.priceChangeMap = null;
|
|
237
248
|
}
|
|
238
249
|
}
|
|
239
|
-
|
|
240
250
|
module.exports = UnskilledCohortFlow;
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview HELIX Product Line (Pass 4)
|
|
3
3
|
*
|
|
4
|
-
* This
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* to generate actionable signals.
|
|
8
|
-
*
|
|
9
|
-
* HYPOTHESIS:
|
|
10
|
-
* - If Herd is in Euphoria (> +8) AND Winners are Selling = "Strong Sell"
|
|
11
|
-
* - If Herd is in Capitulation (< -8) AND Winners are Buying = "Strong Buy"
|
|
4
|
+
* This is the final, stateless signal generator for the HELIX line.
|
|
5
|
+
* It answers: "Are 'Winners' (in-profit) and 'Losers'
|
|
6
|
+
* (in-loss) cohorts flowing in opposite directions?"
|
|
12
7
|
*/
|
|
13
8
|
class HelixContrarianSignal {
|
|
14
9
|
|
|
10
|
+
// --- STANDARD 2: ADDED ---
|
|
11
|
+
constructor() {
|
|
12
|
+
this.result = {};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
15
|
/**
|
|
16
16
|
* Defines the output schema for this calculation.
|
|
17
|
-
* @returns {object} JSON Schema object
|
|
18
17
|
*/
|
|
19
18
|
static getSchema() {
|
|
20
19
|
const tickerSchema = {
|
|
@@ -22,30 +21,22 @@ class HelixContrarianSignal {
|
|
|
22
21
|
"properties": {
|
|
23
22
|
"signal": {
|
|
24
23
|
"type": "string",
|
|
25
|
-
"enum": ["
|
|
26
|
-
},
|
|
27
|
-
"consensus_score": {
|
|
28
|
-
"type": "number",
|
|
29
|
-
"description": "The Herd Consensus Score (-10 to +10)."
|
|
24
|
+
"enum": ["Strong Contrarian", "Contrarian", "Neutral", "Consensus"]
|
|
30
25
|
},
|
|
31
|
-
"
|
|
26
|
+
"helix_score": {
|
|
32
27
|
"type": "number",
|
|
33
|
-
"description": "
|
|
28
|
+
"description": "Divergence score (-10 to +10). Positive = Contrarian (Winners sell, Losers buy), Negative = Consensus (Both buy/sell)."
|
|
34
29
|
},
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
"description": "Net unique losers who joined/left the asset."
|
|
38
|
-
}
|
|
30
|
+
"winner_net_flow": { "type": "number" },
|
|
31
|
+
"loser_net_flow": { "type": "number" }
|
|
39
32
|
},
|
|
40
|
-
"required": ["signal", "
|
|
33
|
+
"required": ["signal", "helix_score", "winner_net_flow", "loser_net_flow"]
|
|
41
34
|
};
|
|
42
35
|
|
|
43
36
|
return {
|
|
44
37
|
"type": "object",
|
|
45
|
-
"description": "Generates final contrarian
|
|
46
|
-
"patternProperties": {
|
|
47
|
-
"^.*$": tickerSchema // Ticker
|
|
48
|
-
},
|
|
38
|
+
"description": "Generates a final contrarian signal based on Winner vs. Loser flow.",
|
|
39
|
+
"patternProperties": { "^.*$": tickerSchema },
|
|
49
40
|
"additionalProperties": tickerSchema
|
|
50
41
|
};
|
|
51
42
|
}
|
|
@@ -68,86 +59,103 @@ class HelixContrarianSignal {
|
|
|
68
59
|
*/
|
|
69
60
|
static getDependencies() {
|
|
70
61
|
return [
|
|
71
|
-
'winner-loser-flow',
|
|
72
|
-
'herd-consensus-score'
|
|
62
|
+
'winner-loser-flow', // from helix (Pass 2)
|
|
63
|
+
'herd-consensus-score' // from helix (Pass 2)
|
|
73
64
|
];
|
|
74
65
|
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Simple tanh normalization. Scales any number to a -10 to +10 range.
|
|
69
|
+
*/
|
|
70
|
+
_normalize(score) {
|
|
71
|
+
// Scale input score to tune sensitivity
|
|
72
|
+
// A score of 50 will be ~9.9 (strong signal)
|
|
73
|
+
// A score of 20 will be ~9.6
|
|
74
|
+
// A score of 10 will be ~7.6
|
|
75
|
+
return Math.tanh(score / 10.0) * 10;
|
|
76
|
+
}
|
|
75
77
|
|
|
76
78
|
/**
|
|
77
79
|
* This is a 'meta' calculation. It runs once.
|
|
78
80
|
*/
|
|
79
|
-
|
|
81
|
+
// --- STANDARD 1: UPDATED SIGNATURE (added rootData) ---
|
|
82
|
+
async process(dateStr, rootData, dependencies, config, fetchedDependencies) {
|
|
80
83
|
const { logger } = dependencies;
|
|
81
84
|
|
|
82
|
-
const
|
|
83
|
-
const
|
|
85
|
+
const cohortFlows = fetchedDependencies['winner-loser-flow'];
|
|
86
|
+
const herdConviction = fetchedDependencies['herd-consensus-score'];
|
|
84
87
|
|
|
85
|
-
if (!
|
|
86
|
-
logger.log('WARN', `[helix/helix-contrarian-signal] Missing dependencies for ${dateStr}
|
|
87
|
-
|
|
88
|
+
if (!cohortFlows || !herdConviction) {
|
|
89
|
+
logger.log('WARN', `[helix/helix-contrarian-signal] Missing dependencies for ${dateStr}.`);
|
|
90
|
+
// --- STANDARD 2: DO NOT RETURN ---
|
|
91
|
+
this.result = {};
|
|
92
|
+
return;
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
const allTickers = new Set([
|
|
91
|
-
...Object.keys(
|
|
92
|
-
...Object.keys(
|
|
96
|
+
...Object.keys(cohortFlows),
|
|
97
|
+
...Object.keys(herdConviction)
|
|
93
98
|
]);
|
|
94
|
-
|
|
99
|
+
|
|
95
100
|
const result = {};
|
|
96
|
-
const CONSENSUS_THRESHOLD = 8.0; // Score for "Extreme"
|
|
97
|
-
const FLOW_THRESHOLD = 5; // Min 5 users moving
|
|
98
101
|
|
|
99
102
|
for (const ticker of allTickers) {
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
const loser_flow = flowData[ticker]?.net_loser_flow || 0;
|
|
103
|
-
|
|
104
|
-
let signal = "Neutral";
|
|
103
|
+
const flow = cohortFlows[ticker];
|
|
104
|
+
const conviction = herdConviction[ticker];
|
|
105
105
|
|
|
106
|
-
//
|
|
106
|
+
// 1. Get Winner Flow (using net_winner_flow)
|
|
107
|
+
const winner_flow = flow?.net_winner_flow || 0;
|
|
107
108
|
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
//
|
|
109
|
+
// 2. Get Loser Flow (using herd_conviction_score)
|
|
110
|
+
// This is a proxy for "Loser Flow".
|
|
111
|
+
// If conviction > 0, losers are buying.
|
|
112
|
+
// If conviction < 0, losers are selling.
|
|
113
|
+
const loser_flow = conviction?.herd_conviction_score || 0;
|
|
114
|
+
|
|
115
|
+
// --- Signal Logic ---
|
|
116
|
+
// A contrarian signal (positive score) happens when:
|
|
117
|
+
// 1. Winners SELL (flow < 0) and Losers BUY (flow > 0)
|
|
118
|
+
//
|
|
119
|
+
// A consensus signal (negative score) happens when:
|
|
120
|
+
// 2. Winners BUY (flow > 0) and Losers BUY (flow > 0)
|
|
121
|
+
// 3. Winners SELL (flow < 0) and Losers SELL (flow < 0)
|
|
118
122
|
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// --- Tertiary "Dumb Money" Signals (Warning signs) ---
|
|
123
|
+
// We can simplify this:
|
|
124
|
+
// Score = Loser Flow - Winner Flow
|
|
125
|
+
//
|
|
126
|
+
// Case 1: Loser (Buy, +50) - Winner (Sell, -20) = +70 (Strong Contrarian)
|
|
127
|
+
// Case 2: Loser (Buy, +50) - Winner (Buy, +20) = +30 (Weak Contrarian)
|
|
128
|
+
// Case 3: Loser (Sell, -50) - Winner (Sell, -20) = -30 (Consensus)
|
|
129
|
+
// Case 4: Loser (Sell, -50) - Winner (Buy, +20) = -70 (Strong Consensus)
|
|
129
130
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
else if (
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
net_winner_flow: winner_flow,
|
|
145
|
-
net_loser_flow: loser_flow
|
|
146
|
-
};
|
|
147
|
-
}
|
|
131
|
+
const divergence = loser_flow - winner_flow;
|
|
132
|
+
const helix_score = this._normalize(divergence);
|
|
133
|
+
|
|
134
|
+
let signal = "Neutral";
|
|
135
|
+
if (helix_score > 7.5) signal = "Strong Contrarian"; // Losers buying, Winners selling
|
|
136
|
+
else if (helix_score > 2.5) signal = "Contrarian";
|
|
137
|
+
else if (helix_score < -2.5) signal = "Consensus"; // Both moving same way
|
|
138
|
+
|
|
139
|
+
result[ticker] = {
|
|
140
|
+
signal: signal,
|
|
141
|
+
helix_score: helix_score,
|
|
142
|
+
winner_net_flow: winner_flow,
|
|
143
|
+
loser_net_flow: loser_flow // Using conviction as flow proxy
|
|
144
|
+
};
|
|
148
145
|
}
|
|
149
146
|
|
|
150
|
-
|
|
147
|
+
// --- STANDARD 2: SET STATE, DO NOT RETURN ---
|
|
148
|
+
this.result = result;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// --- STANDARD 2: ADDED ---
|
|
152
|
+
async getResult(fetchedDependencies) {
|
|
153
|
+
return this.result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// --- STANDARD 2: ADDED ---
|
|
157
|
+
reset() {
|
|
158
|
+
this.result = {};
|
|
151
159
|
}
|
|
152
160
|
}
|
|
153
161
|
|