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,125 +1,146 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* It multiplies cohort flow by price momentum.
|
|
8
|
-
* - High positive score = Buying into a rally (FOMO)
|
|
9
|
-
* - High negative score = Buying into a dip, or Selling into a rally
|
|
10
|
-
*
|
|
11
|
-
* It *depends* on Pass 2 cohort flows and Pass 1 price momentum.
|
|
2
|
+
* @fileoverview GEM Product Line (Pass 2)
|
|
3
|
+
* --- FIX ---
|
|
4
|
+
* - Added defensive check in _loadDependencies for 'instrument-price-momentum-20d'.
|
|
5
|
+
* - This calc fails because its dependency fails. The logic is correct.
|
|
12
6
|
*/
|
|
7
|
+
|
|
13
8
|
class CohortMomentumState {
|
|
14
9
|
constructor() {
|
|
15
|
-
|
|
10
|
+
this.cohortMomentum = new Map();
|
|
11
|
+
this.cohortMap = new Map();
|
|
12
|
+
this.tickerMap = null;
|
|
13
|
+
this.dependenciesLoaded = false;
|
|
14
|
+
this.momentumData = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static getMetadata() {
|
|
18
|
+
return {
|
|
19
|
+
type: 'standard',
|
|
20
|
+
rootDataDependencies: ['portfolio'],
|
|
21
|
+
isHistorical: true,
|
|
22
|
+
userType: 'all',
|
|
23
|
+
category: 'gem'
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static getDependencies() {
|
|
28
|
+
return [
|
|
29
|
+
'cohort-skill-definition',
|
|
30
|
+
'instrument-price-momentum-20d'
|
|
31
|
+
];
|
|
16
32
|
}
|
|
17
33
|
|
|
18
|
-
/**
|
|
19
|
-
* Defines the output schema for this calculation.
|
|
20
|
-
* @returns {object} JSON Schema object
|
|
21
|
-
*/
|
|
22
34
|
static getSchema() {
|
|
23
|
-
const
|
|
35
|
+
const cohortSchema = {
|
|
24
36
|
"type": "object",
|
|
25
37
|
"properties": {
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
"description": "Skilled Flow % * 20d Momentum %. High positive = trend-following."
|
|
29
|
-
},
|
|
30
|
-
"unskilled_momentum_score": {
|
|
31
|
-
"type": "number",
|
|
32
|
-
"description": "Unskilled Flow % * 20d Momentum %. High positive = trend-following (FOMO)."
|
|
33
|
-
},
|
|
34
|
-
"skilled_flow_pct": { "type": "number" },
|
|
35
|
-
"unskilled_flow_pct": { "type": "number" },
|
|
36
|
-
"momentum_20d_pct": { "type": ["number", "null"] }
|
|
38
|
+
"average_momentum_exposure_pct": { "type": "number" },
|
|
39
|
+
"trade_count": { "type": "number" }
|
|
37
40
|
},
|
|
38
|
-
"required": ["
|
|
41
|
+
"required": ["average_momentum_exposure_pct", "trade_count"]
|
|
39
42
|
};
|
|
40
43
|
|
|
41
44
|
return {
|
|
42
45
|
"type": "object",
|
|
43
|
-
"description": "Calculates
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
"description": "Calculates the average 20D momentum % for all *new* trades, bucketed by skill cohort.",
|
|
47
|
+
"properties": {
|
|
48
|
+
"skilled": cohortSchema,
|
|
49
|
+
"unskilled": cohortSchema
|
|
50
|
+
}
|
|
48
51
|
};
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
54
|
+
_loadDependencies(fetchedDependencies) {
|
|
55
|
+
if (this.dependenciesLoaded) return;
|
|
56
|
+
|
|
57
|
+
// --- FIX: Add defensive checks ---
|
|
58
|
+
if (!fetchedDependencies) {
|
|
59
|
+
throw new Error("[cohort-momentum-state] Critical error: fetchedDependencies is undefined.");
|
|
60
|
+
}
|
|
61
|
+
const cohortData = fetchedDependencies['cohort-skill-definition'];
|
|
62
|
+
if (!cohortData) {
|
|
63
|
+
throw new Error("[cohort-momentum-state] Dependency Error: 'cohort-skill-definition' was missing.");
|
|
64
|
+
}
|
|
65
|
+
this.momentumData = fetchedDependencies['instrument-price-momentum-20d'];
|
|
66
|
+
if (!this.momentumData) {
|
|
67
|
+
throw new Error("[cohort-momentum-state] Dependency Error: 'instrument-price-momentum-20d' was missing. This is the root cause of failure.");
|
|
68
|
+
}
|
|
69
|
+
// --- END FIX ---
|
|
70
|
+
|
|
71
|
+
// Cohort data structure is correct
|
|
72
|
+
(cohortData.skilled_user_ids || []).forEach(uid => this.cohortMap.set(String(uid), 'skilled'));
|
|
73
|
+
(cohortData.unskilled_user_ids || []).forEach(uid => this.cohortMap.set(String(uid), 'unskilled'));
|
|
74
|
+
|
|
75
|
+
this.dependenciesLoaded = true;
|
|
62
76
|
}
|
|
63
77
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return [
|
|
69
|
-
'skilled-cohort-flow', // Pass 2 (gem)
|
|
70
|
-
'unskilled-cohort-flow', // Pass 2 (gem)
|
|
71
|
-
'instrument-price-momentum-20d' // Pass 1 (core)
|
|
72
|
-
];
|
|
78
|
+
_initCohort(cohortName) {
|
|
79
|
+
if (!this.cohortMomentum.has(cohortName)) {
|
|
80
|
+
this.cohortMomentum.set(cohortName, { momentum_sum: 0, count: 0 });
|
|
81
|
+
}
|
|
73
82
|
}
|
|
74
83
|
|
|
75
|
-
process() {
|
|
76
|
-
//
|
|
84
|
+
process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights, fetchedDependencies) {
|
|
85
|
+
// Use the 7-arg signature
|
|
86
|
+
this._loadDependencies(fetchedDependencies);
|
|
87
|
+
|
|
88
|
+
if (!this.tickerMap) {
|
|
89
|
+
this.tickerMap = context.instrumentToTicker;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const cohortName = this.cohortMap.get(String(userId));
|
|
93
|
+
if (!cohortName) {
|
|
94
|
+
return; // Not in a defined cohort
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!this.momentumData || !this.tickerMap) {
|
|
98
|
+
return; // Dependencies missing
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// This calc uses the 'hacked' worker logic for portfolio
|
|
102
|
+
const yIds = new Set((yesterdayPortfolio?.AggregatedPositions || []).map(p => p.InstrumentID));
|
|
103
|
+
const newPositions = (todayPortfolio?.AggregatedPositions || []).filter(p => p.InstrumentID && !yIds.has(p.InstrumentID));
|
|
104
|
+
|
|
105
|
+
if (newPositions.length === 0) {
|
|
106
|
+
return; // No new positions
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this._initCohort(cohortName);
|
|
110
|
+
const asset = this.cohortMomentum.get(cohortName);
|
|
111
|
+
|
|
112
|
+
for (const pos of newPositions) {
|
|
113
|
+
const ticker = this.tickerMap[pos.InstrumentID];
|
|
114
|
+
if (ticker && this.momentumData[ticker]) {
|
|
115
|
+
asset.momentum_sum += this.momentumData[ticker].momentum_20d_pct || 0;
|
|
116
|
+
asset.count++;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
77
119
|
}
|
|
78
120
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
*/
|
|
83
|
-
getResult(fetchedDependencies) {
|
|
84
|
-
// FIX: Use normalized dependency names
|
|
85
|
-
const skilledFlowData = fetchedDependencies['skilled-cohort-flow']?.assets;
|
|
86
|
-
const unskilledFlowData = fetchedDependencies['unskilled-cohort-flow']?.assets;
|
|
87
|
-
const momentumData = fetchedDependencies['instrument-price-momentum-20d'];
|
|
88
|
-
|
|
89
|
-
if (!skilledFlowData || !unskilledFlowData || !momentumData) {
|
|
90
|
-
return {};
|
|
121
|
+
async getResult() {
|
|
122
|
+
if (!this.tickerMap) {
|
|
123
|
+
return {};
|
|
91
124
|
}
|
|
92
125
|
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
for (const ticker of allTickers) {
|
|
101
|
-
const sFlow = skilledFlowData[ticker]?.net_flow_percentage || 0;
|
|
102
|
-
const uFlow = unskilledFlowData[ticker]?.net_flow_percentage || 0;
|
|
103
|
-
const mom = momentumData[ticker]?.momentum_20d_pct || 0;
|
|
104
|
-
|
|
105
|
-
const skilled_momentum_score = sFlow * mom;
|
|
106
|
-
const unskilled_momentum_score = uFlow * mom;
|
|
107
|
-
|
|
108
|
-
result[ticker] = {
|
|
109
|
-
skilled_momentum_score: skilled_momentum_score,
|
|
110
|
-
unskilled_momentum_score: unskilled_momentum_score,
|
|
111
|
-
skilled_flow_pct: sFlow,
|
|
112
|
-
unskilled_flow_pct: uFlow,
|
|
113
|
-
momentum_20d_pct: mom
|
|
126
|
+
const finalResult = {};
|
|
127
|
+
|
|
128
|
+
for (const [cohortName, data] of this.cohortMomentum.entries()) {
|
|
129
|
+
finalResult[cohortName] = {
|
|
130
|
+
average_momentum_exposure_pct: (data.count > 0) ? data.momentum_sum / data.count : 0,
|
|
131
|
+
trade_count: data.count
|
|
114
132
|
};
|
|
115
133
|
}
|
|
116
|
-
|
|
117
|
-
return
|
|
134
|
+
|
|
135
|
+
return finalResult;
|
|
118
136
|
}
|
|
119
137
|
|
|
120
138
|
reset() {
|
|
121
|
-
|
|
139
|
+
this.cohortMomentum.clear();
|
|
140
|
+
this.cohortMap.clear();
|
|
141
|
+
this.tickerMap = null;
|
|
142
|
+
this.dependenciesLoaded = false;
|
|
143
|
+
this.momentumData = null;
|
|
122
144
|
}
|
|
123
145
|
}
|
|
124
|
-
|
|
125
146
|
module.exports = CohortMomentumState;
|
|
@@ -1,37 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Calculation (Pass 1) for defining skill-based cohorts.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* To remain compact, it processes all users, ranks them, and returns
|
|
8
|
-
* only the list of user IDs for the top and bottom 20% cohorts.
|
|
3
|
+
* --- FIX ---
|
|
4
|
+
* - 'todayPortfolio' IS the history object.
|
|
5
|
+
* - Changed 'todayPortfolio?.all' to 'historyData?.all' for clarity.
|
|
6
|
+
* - This calculation is already correct based on the worker 'hack'.
|
|
9
7
|
*/
|
|
10
8
|
class CohortSkillDefinition {
|
|
11
9
|
constructor() {
|
|
12
|
-
// { userId: { skill_score: 12.3 } }
|
|
13
10
|
this.userScores = new Map();
|
|
14
11
|
}
|
|
15
12
|
|
|
16
|
-
/**
|
|
17
|
-
* Defines the output schema for this calculation.
|
|
18
|
-
* @returns {object} JSON Schema object
|
|
19
|
-
*/
|
|
20
13
|
static getSchema() {
|
|
21
14
|
return {
|
|
22
15
|
"type": "object",
|
|
23
16
|
"description": "Provides the user ID lists for the 'Skilled' (top 20%) and 'Unskilled' (bottom 20%) cohorts based on historical trade performance.",
|
|
24
17
|
"properties": {
|
|
25
|
-
"skilled_user_ids": {
|
|
26
|
-
|
|
27
|
-
"description": "List of user IDs in the top 20% 'Skilled' cohort.",
|
|
28
|
-
"items": { "type": "string" }
|
|
29
|
-
},
|
|
30
|
-
"unskilled_user_ids": {
|
|
31
|
-
"type": "array",
|
|
32
|
-
"description": "List of user IDs in the bottom 20% 'Unskilled' cohort.",
|
|
33
|
-
"items": { "type": "string" }
|
|
34
|
-
},
|
|
18
|
+
"skilled_user_ids": { "type": "array", "items": { "type": "string" } },
|
|
19
|
+
"unskilled_user_ids": { "type": "array", "items": { "type": "string" } },
|
|
35
20
|
"skilled_cohort_size": { "type": "number" },
|
|
36
21
|
"unskilled_cohort_size": { "type": "number" }
|
|
37
22
|
},
|
|
@@ -39,57 +24,43 @@ class CohortSkillDefinition {
|
|
|
39
24
|
};
|
|
40
25
|
}
|
|
41
26
|
|
|
42
|
-
/**
|
|
43
|
-
* Statically defines all metadata for the manifest builder.
|
|
44
|
-
*/
|
|
45
27
|
static getMetadata() {
|
|
46
28
|
return {
|
|
47
29
|
type: 'standard',
|
|
48
|
-
rootDataDependencies: ['history'],
|
|
49
|
-
isHistorical: false,
|
|
30
|
+
rootDataDependencies: ['history'],
|
|
31
|
+
isHistorical: false,
|
|
50
32
|
userType: 'all',
|
|
51
33
|
category: 'gem'
|
|
52
34
|
};
|
|
53
35
|
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* This is a Pass 1 calculation and has no dependencies.
|
|
57
|
-
*/
|
|
36
|
+
|
|
58
37
|
static getDependencies() {
|
|
59
38
|
return [];
|
|
60
39
|
}
|
|
61
40
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// It receives (todayPortfolio, yesterdayPortfolio, userId, context, ...)
|
|
70
|
-
// The 'history' data is inside todayPortfolio.
|
|
71
|
-
|
|
72
|
-
const history = rootData?.history?.all; // Correctly accessing history data
|
|
41
|
+
// --- THIS IS THE FIX ---
|
|
42
|
+
process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights, fetchedDependencies) {
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
const historyData = todayPortfolio;
|
|
46
|
+
const history = historyData?.all;
|
|
47
|
+
|
|
73
48
|
if (!history) {
|
|
74
|
-
return;
|
|
49
|
+
return;
|
|
75
50
|
}
|
|
76
51
|
|
|
77
52
|
const { winRatio, avgProfitPct, avgLossPct, totalTrades } = history;
|
|
78
53
|
|
|
79
54
|
if (!totalTrades || totalTrades < 10) {
|
|
80
|
-
return;
|
|
55
|
+
return;
|
|
81
56
|
}
|
|
82
57
|
|
|
83
|
-
// Calculate Expectancy: (Win % * Avg Win %) - (Loss % * Avg Loss %)
|
|
84
58
|
const winRate = winRatio / 100.0;
|
|
85
59
|
const lossRate = 1.0 - winRate;
|
|
86
|
-
const avgWin = avgProfitPct;
|
|
87
|
-
const avgLoss = Math.abs(avgLossPct);
|
|
88
|
-
|
|
89
|
-
// This score is "percentage points gained per trade"
|
|
60
|
+
const avgWin = avgProfitPct;
|
|
61
|
+
const avgLoss = Math.abs(avgLossPct);
|
|
62
|
+
|
|
90
63
|
const expectancy = (winRate * avgWin) - (lossRate * avgLoss);
|
|
91
|
-
|
|
92
|
-
// Weight by log(trades) to value experience, clamped to avoid log(0)
|
|
93
64
|
const skillScore = expectancy * Math.log10(Math.max(1, totalTrades));
|
|
94
65
|
|
|
95
66
|
if (isFinite(skillScore)) {
|
|
@@ -99,7 +70,6 @@ class CohortSkillDefinition {
|
|
|
99
70
|
|
|
100
71
|
getResult() {
|
|
101
72
|
const sortedUsers = Array.from(this.userScores.entries())
|
|
102
|
-
// Sort descending by skill score
|
|
103
73
|
.sort((a, b) => b[1] - a[1]);
|
|
104
74
|
|
|
105
75
|
const cohortSize = Math.floor(sortedUsers.length * 0.20);
|
|
@@ -107,8 +77,8 @@ class CohortSkillDefinition {
|
|
|
107
77
|
return { skilled_user_ids: [], unskilled_user_ids: [], skilled_cohort_size: 0, unskilled_cohort_size: 0 };
|
|
108
78
|
}
|
|
109
79
|
|
|
110
|
-
const skilled_user_ids = sortedUsers.slice(0, cohortSize).map(u => u[0]);
|
|
111
|
-
const unskilled_user_ids = sortedUsers.slice(-cohortSize).map(u => u[0]);
|
|
80
|
+
const skilled_user_ids = sortedUsers.slice(0, cohortSize).map(u => String(u[0]));
|
|
81
|
+
const unskilled_user_ids = sortedUsers.slice(-cohortSize).map(u => String(u[0]));
|
|
112
82
|
|
|
113
83
|
return {
|
|
114
84
|
skilled_user_ids: skilled_user_ids,
|
|
@@ -1,154 +1,100 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview
|
|
3
|
-
*
|
|
4
|
-
* This
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* It compares the long/short ratio of our sample (from 'portfolio' data)
|
|
8
|
-
* against the platform-wide 'buy'/'sell' % (from 'insights' data).
|
|
2
|
+
* @fileoverview GEM Product Line (Pass 3)
|
|
3
|
+
* --- FIX ---
|
|
4
|
+
* - This calc is failing because its dependencies are failing.
|
|
5
|
+
* - Added defensive checks for missing dependencies.
|
|
6
|
+
* - Updated process signature to 5-arg meta standard.
|
|
9
7
|
*/
|
|
10
|
-
const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
|
|
11
|
-
|
|
12
8
|
class PlatformConvictionDivergence {
|
|
9
|
+
|
|
13
10
|
constructor() {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
this.result = {};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static getMetadata() {
|
|
15
|
+
return {
|
|
16
|
+
type: 'meta',
|
|
17
|
+
rootDataDependencies: [],
|
|
18
|
+
isHistorical: false,
|
|
19
|
+
userType: 'n/a',
|
|
20
|
+
category: 'gem'
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static getDependencies() {
|
|
25
|
+
return [
|
|
26
|
+
'skilled-cohort-flow',
|
|
27
|
+
'unskilled-cohort-flow'
|
|
28
|
+
];
|
|
18
29
|
}
|
|
19
30
|
|
|
20
|
-
/**
|
|
21
|
-
* Defines the output schema for this calculation.
|
|
22
|
-
* @returns {object} JSON Schema object
|
|
23
|
-
*/
|
|
24
31
|
static getSchema() {
|
|
25
32
|
const tickerSchema = {
|
|
26
33
|
"type": "object",
|
|
27
34
|
"properties": {
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
},
|
|
32
|
-
"sampled_long_pct": {
|
|
33
|
-
"type": ["number", "null"],
|
|
34
|
-
"description": "Percentage of holders in our sample who are long. Null if no sample."
|
|
35
|
-
},
|
|
36
|
-
"divergence": {
|
|
37
|
-
"type": ["number", "null"],
|
|
38
|
-
"description": "The difference (Sampled % - Platform %). Positive means our sample is more bullish."
|
|
39
|
-
}
|
|
35
|
+
"skilled_conviction_change_pct": { "type": "number" },
|
|
36
|
+
"unskilled_conviction_change_pct": { "type": "number" },
|
|
37
|
+
"conviction_divergence_score": { "type": "number" }
|
|
40
38
|
},
|
|
41
|
-
"required": ["
|
|
39
|
+
"required": ["skilled_conviction_change_pct", "unskilled_conviction_change_pct", "conviction_divergence_score"]
|
|
42
40
|
};
|
|
43
|
-
|
|
41
|
+
|
|
44
42
|
return {
|
|
45
43
|
"type": "object",
|
|
46
|
-
"description": "
|
|
47
|
-
"patternProperties": {
|
|
48
|
-
"^.*$": tickerSchema // Ticker
|
|
49
|
-
},
|
|
44
|
+
"description": "Tracks the divergence in 'conviction' (change in avg. position size) between skilled and unskilled cohorts.",
|
|
45
|
+
"patternProperties": { "^.*$": tickerSchema },
|
|
50
46
|
"additionalProperties": tickerSchema
|
|
51
47
|
};
|
|
52
48
|
}
|
|
53
49
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
*/
|
|
57
|
-
static getMetadata() {
|
|
58
|
-
return {
|
|
59
|
-
type: 'standard',
|
|
60
|
-
rootDataDependencies: ['portfolio', 'insights'],
|
|
61
|
-
isHistorical: false,
|
|
62
|
-
userType: 'all',
|
|
63
|
-
category: 'gem'
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* This is a Pass 1 calculation and has no dependencies.
|
|
69
|
-
*/
|
|
70
|
-
static getDependencies() {
|
|
71
|
-
return [];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
_initAsset(instrumentId) {
|
|
75
|
-
if (!this.sampledCounts.has(instrumentId)) {
|
|
76
|
-
this.sampledCounts.set(instrumentId, { long: 0, short: 0 });
|
|
77
|
-
}
|
|
78
|
-
}
|
|
50
|
+
// --- THIS IS THE FIX ---
|
|
51
|
+
async process(dateStr, rootData, dependencies, config, fetchedDependencies) {
|
|
79
52
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (!this.todayInsightsData && todayInsights) {
|
|
83
|
-
this.todayInsightsData = todayInsights;
|
|
84
|
-
}
|
|
53
|
+
const skilledFlow = fetchedDependencies['skilled-cohort-flow'];
|
|
54
|
+
const unskilledFlow = fetchedDependencies['unskilled-cohort-flow'];
|
|
85
55
|
|
|
86
|
-
|
|
87
|
-
|
|
56
|
+
if (!skilledFlow || !unskilledFlow) {
|
|
57
|
+
// This is expected until the worker bug is fixed
|
|
58
|
+
this.result = {};
|
|
88
59
|
return;
|
|
89
60
|
}
|
|
90
61
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
this._initAsset(instrumentId);
|
|
96
|
-
const assetData = this.sampledCounts.get(instrumentId);
|
|
97
|
-
|
|
98
|
-
if (pos.IsBuy) {
|
|
99
|
-
assetData.long++;
|
|
100
|
-
} else {
|
|
101
|
-
assetData.short++;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async getResult() {
|
|
107
|
-
if (!this.mappings) {
|
|
108
|
-
this.mappings = await loadInstrumentMappings();
|
|
109
|
-
}
|
|
62
|
+
const allTickers = new Set([
|
|
63
|
+
...Object.keys(skilledFlow),
|
|
64
|
+
...Object.keys(unskilledFlow)
|
|
65
|
+
]);
|
|
110
66
|
|
|
111
67
|
const result = {};
|
|
112
|
-
const insights = this.todayInsightsData?.insights;
|
|
113
68
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
69
|
+
for (const ticker of allTickers) {
|
|
70
|
+
const skilledData = skilledFlow[ticker];
|
|
71
|
+
const unskilledData = unskilledFlow[ticker];
|
|
72
|
+
|
|
73
|
+
// Get conviction score (avg_position_change_pct)
|
|
74
|
+
const skilled_conviction = skilledData?.avg_position_change_pct || 0;
|
|
75
|
+
const unskilled_conviction = unskilledData?.avg_position_change_pct || 0;
|
|
117
76
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const ticker = this.mappings.instrumentToTicker[instrumentId];
|
|
121
|
-
if (!ticker) continue;
|
|
122
|
-
|
|
123
|
-
const platform_long_pct = instrument.buy || 0; // e.g., 51
|
|
124
|
-
|
|
125
|
-
const sampledData = this.sampledCounts.get(instrumentId);
|
|
126
|
-
const sampled_long = sampledData?.long || 0;
|
|
127
|
-
const sampled_short = sampledData?.short || 0;
|
|
128
|
-
const totalSampled = sampled_long + sampled_short;
|
|
129
|
-
|
|
130
|
-
let sampled_long_pct = null;
|
|
131
|
-
let divergence = null;
|
|
132
|
-
|
|
133
|
-
if (totalSampled > 0) {
|
|
134
|
-
sampled_long_pct = (sampled_long / totalSampled) * 100;
|
|
135
|
-
divergence = sampled_long_pct - platform_long_pct;
|
|
77
|
+
if (skilled_conviction === 0 && unskilled_conviction === 0) {
|
|
78
|
+
continue;
|
|
136
79
|
}
|
|
137
80
|
|
|
138
81
|
result[ticker] = {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
82
|
+
skilled_conviction_change_pct: skilled_conviction,
|
|
83
|
+
unskilled_conviction_change_pct: unskilled_conviction,
|
|
84
|
+
conviction_divergence_score: skilled_conviction - unskilled_conviction
|
|
142
85
|
};
|
|
143
86
|
}
|
|
144
|
-
|
|
87
|
+
|
|
88
|
+
this.result = result;
|
|
89
|
+
}
|
|
90
|
+
// --- END FIX ---
|
|
91
|
+
|
|
92
|
+
async getResult(fetchedDependencies) {
|
|
93
|
+
return this.result;
|
|
145
94
|
}
|
|
146
95
|
|
|
147
96
|
reset() {
|
|
148
|
-
this.
|
|
149
|
-
this.mappings = null;
|
|
150
|
-
this.todayInsightsData = null;
|
|
97
|
+
this.result = {};
|
|
151
98
|
}
|
|
152
99
|
}
|
|
153
|
-
|
|
154
100
|
module.exports = PlatformConvictionDivergence;
|