aiden-shared-calculations-unified 1.0.71 → 1.0.73
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/behavioural/historical/asset_crowd_flow.js +2 -2
- package/calculations/behavioural/historical/dumb-cohort-flow.js +1 -1
- package/calculations/behavioural/historical/gem_cohort-skill-definition.js +109 -0
- package/calculations/behavioural/historical/gem_skilled-cohort-flow.js +226 -0
- package/calculations/behavioural/historical/gem_unskilled-cohort-flow.js +225 -0
- package/calculations/insights/daily_ownership_per_sector.js +1 -1
- package/calculations/meta/crowd_sharpe_ratio_proxy.js +31 -18
- package/calculations/meta/gem_cohort-momentum-state.js +110 -0
- package/calculations/meta/gem_instrument-price-momentum.js +115 -0
- package/calculations/meta/gem_skilled-unskilled-divergence.js +139 -0
- package/calculations/meta/quant-skill-alpha-signal.js +152 -0
- package/calculations/meta/social-topic-driver-index.js +56 -9
- package/calculations/meta/social-topic-predictive-potential.js +9 -3
- package/calculations/pnl/pnl_distribution_per_stock.js +70 -16
- package/calculations/sentiment/gem_platform-conviction-divergence.js +141 -0
- package/package.json +3 -2
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Calculation (Pass 3) for cohort momentum state.
|
|
3
|
+
*
|
|
4
|
+
* This metric answers: "Are the 'Skilled' and 'Unskilled' cohorts
|
|
5
|
+
* trend-following or acting as contrarians?"
|
|
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.
|
|
12
|
+
*/
|
|
13
|
+
class CohortMomentumState {
|
|
14
|
+
constructor() {
|
|
15
|
+
// No per-user processing
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Defines the output schema for this calculation.
|
|
20
|
+
* @returns {object} JSON Schema object
|
|
21
|
+
*/
|
|
22
|
+
static getSchema() {
|
|
23
|
+
const tickerSchema = {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"properties": {
|
|
26
|
+
"skilled_momentum_score": {
|
|
27
|
+
"type": "number",
|
|
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"] }
|
|
37
|
+
},
|
|
38
|
+
"required": ["skilled_momentum_score", "unskilled_momentum_score"]
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"description": "Calculates a momentum-following score for Skilled and Unskilled cohorts.",
|
|
44
|
+
"patternProperties": {
|
|
45
|
+
"^.*$": tickerSchema // Ticker
|
|
46
|
+
},
|
|
47
|
+
"additionalProperties": tickerSchema
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Statically declare dependencies.
|
|
53
|
+
*/
|
|
54
|
+
static getDependencies() {
|
|
55
|
+
return [
|
|
56
|
+
'gem_skilled-cohort-flow', // Pass 2
|
|
57
|
+
'gem_unskilled-cohort-flow', // Pass 2
|
|
58
|
+
'gem_instrument-price-momentum' // Pass 1
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
process() {
|
|
63
|
+
// No-op
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* This is a 'meta' calculation.
|
|
68
|
+
* @param {object} fetchedDependencies - Results from previous passes.
|
|
69
|
+
*/
|
|
70
|
+
getResult(fetchedDependencies) {
|
|
71
|
+
const skilledFlowData = fetchedDependencies['skilled-cohort-flow']?.assets;
|
|
72
|
+
const unskilledFlowData = fetchedDependencies['unskilled-cohort-flow']?.assets;
|
|
73
|
+
const momentumData = fetchedDependencies['instrument-price-momentum'];
|
|
74
|
+
|
|
75
|
+
if (!skilledFlowData || !unskilledFlowData || !momentumData) {
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const result = {};
|
|
80
|
+
const allTickers = new Set([
|
|
81
|
+
...Object.keys(skilledFlowData),
|
|
82
|
+
...Object.keys(unskilledFlowData)
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
for (const ticker of allTickers) {
|
|
86
|
+
const sFlow = skilledFlowData[ticker]?.net_flow_percentage || 0;
|
|
87
|
+
const uFlow = unskilledFlowData[ticker]?.net_flow_percentage || 0;
|
|
88
|
+
const mom = momentumData[ticker]?.momentum_20d_pct || 0;
|
|
89
|
+
|
|
90
|
+
const skilled_momentum_score = sFlow * mom;
|
|
91
|
+
const unskilled_momentum_score = uFlow * mom;
|
|
92
|
+
|
|
93
|
+
result[ticker] = {
|
|
94
|
+
skilled_momentum_score: skilled_momentum_score,
|
|
95
|
+
unskilled_momentum_score: unskilled_momentum_score,
|
|
96
|
+
skilled_flow_pct: sFlow,
|
|
97
|
+
unskilled_flow_pct: uFlow,
|
|
98
|
+
momentum_20d_pct: mom
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
reset() {
|
|
106
|
+
// No state
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = CohortMomentumState;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Calculation (Pass 1 - Meta) for 20-day price momentum.
|
|
3
|
+
*
|
|
4
|
+
* This metric answers: "What is the 20-day percentage price change for
|
|
5
|
+
* every instrument?"
|
|
6
|
+
*
|
|
7
|
+
* It is a 'meta' calculation that runs once, loads all price data,
|
|
8
|
+
* and provides a reusable momentum signal for downstream passes.
|
|
9
|
+
*/
|
|
10
|
+
const { loadAllPriceData } = require('../../utils/price_data_provider');
|
|
11
|
+
|
|
12
|
+
class InstrumentPriceMomentum {
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Defines the output schema for this calculation.
|
|
16
|
+
* @returns {object} JSON Schema object
|
|
17
|
+
*/
|
|
18
|
+
static getSchema() {
|
|
19
|
+
const tickerSchema = {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"properties": {
|
|
22
|
+
"momentum_20d_pct": {
|
|
23
|
+
"type": ["number", "null"],
|
|
24
|
+
"description": "The 20-day rolling price change percentage."
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"required": ["momentum_20d_pct"]
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"description": "Calculates the 20-day price momentum for all instruments.",
|
|
33
|
+
"patternProperties": {
|
|
34
|
+
"^.*$": tickerSchema // Ticker
|
|
35
|
+
},
|
|
36
|
+
"additionalProperties": tickerSchema
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* This is a Pass 1 calculation and has no dependencies.
|
|
42
|
+
*/
|
|
43
|
+
static getDependencies() {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Helper to get date string N days ago
|
|
48
|
+
_getDateStr(baseDateStr, daysOffset) {
|
|
49
|
+
const date = new Date(baseDateStr + 'T00:00:00Z');
|
|
50
|
+
date.setUTCDate(date.getUTCDate() + daysOffset);
|
|
51
|
+
return date.toISOString().slice(0, 10);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Helper to find the last available price on or before a date
|
|
55
|
+
_findPrice(priceHistory, dateStr, maxLookback = 5) {
|
|
56
|
+
if (!priceHistory) return null;
|
|
57
|
+
let checkDate = new Date(dateStr + 'T00:00:00Z');
|
|
58
|
+
for (let i = 0; i < maxLookback; i++) {
|
|
59
|
+
const checkDateStr = checkDate.toISOString().slice(0, 10);
|
|
60
|
+
const price = priceHistory[checkDateStr];
|
|
61
|
+
if (price !== undefined && price !== null && price > 0) {
|
|
62
|
+
return price;
|
|
63
|
+
}
|
|
64
|
+
checkDate.setUTCDate(checkDate.getUTCDate() - 1);
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* This is a 'meta' calculation. It runs once.
|
|
71
|
+
* @param {string} dateStr - The date string 'YYYY-MM-DD'.
|
|
72
|
+
* @param {object} dependencies - The shared dependencies (e.g., logger, calculationUtils).
|
|
73
|
+
* @param {object} config - The computation system configuration.
|
|
74
|
+
* @param {object} fetchedDependencies - (Unused)
|
|
75
|
+
* @returns {Promise<object>} The calculation result.
|
|
76
|
+
*/
|
|
77
|
+
async process(dateStr, dependencies, config, fetchedDependencies) {
|
|
78
|
+
const { logger, calculationUtils, mappings } = dependencies;
|
|
79
|
+
|
|
80
|
+
// Load all price data and mappings
|
|
81
|
+
const priceMap = await loadAllPriceData();
|
|
82
|
+
const tickerMap = await calculationUtils.loadInstrumentMappings();
|
|
83
|
+
|
|
84
|
+
if (!priceMap || !tickerMap || !tickerMap.instrumentToTicker) {
|
|
85
|
+
logger.log('ERROR', '[instrument-price-momentum] Failed to load priceMap or mappings.');
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const dateStrT20 = this._getDateStr(dateStr, -20);
|
|
90
|
+
const result = {};
|
|
91
|
+
|
|
92
|
+
for (const instrumentId in priceMap) {
|
|
93
|
+
const instrumentPrices = priceMap[instrumentId];
|
|
94
|
+
const ticker = tickerMap.instrumentToTicker[instrumentId];
|
|
95
|
+
|
|
96
|
+
if (!ticker) continue; // Skip if we can't map ID to ticker
|
|
97
|
+
|
|
98
|
+
const priceT = this._findPrice(instrumentPrices, dateStr);
|
|
99
|
+
const priceT20 = this._findPrice(instrumentPrices, dateStrT20);
|
|
100
|
+
|
|
101
|
+
let momentum = null;
|
|
102
|
+
if (priceT && priceT20 && priceT20 > 0) {
|
|
103
|
+
momentum = ((priceT - priceT20) / priceT20) * 100; // As percentage
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
result[ticker] = {
|
|
107
|
+
momentum_20d_pct: momentum
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = InstrumentPriceMomentum;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Calculation (Pass 3) for skilled-unskilled divergence.
|
|
3
|
+
*
|
|
4
|
+
* This metric answers: "What divergence signals (e.g., capitulation,
|
|
5
|
+
* euphoria) can be found by comparing the net asset and sector flow
|
|
6
|
+
* of the 'Skilled Cohort' vs. the 'Unskilled Cohort'?"
|
|
7
|
+
*
|
|
8
|
+
* It *depends* on 'skilled-cohort-flow' and 'unskilled-cohort-flow'.
|
|
9
|
+
*/
|
|
10
|
+
class SkilledUnskilledDivergence {
|
|
11
|
+
constructor() {
|
|
12
|
+
// No per-user processing
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Defines the output schema for this calculation.
|
|
17
|
+
* @returns {object} JSON Schema object
|
|
18
|
+
*/
|
|
19
|
+
static getSchema() {
|
|
20
|
+
const signalSchema = {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"properties": {
|
|
23
|
+
"status": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"enum": ["Capitulation", "Euphoria", "Confirmation (Buy)", "Confirmation (Sell)", "Divergence (Skilled Buy)", "Divergence (Skilled Sell)", "Neutral"]
|
|
26
|
+
},
|
|
27
|
+
"skilled_flow_pct": { "type": "number" },
|
|
28
|
+
"unskilled_flow_pct": { "type": "number" }
|
|
29
|
+
},
|
|
30
|
+
"required": ["status", "skilled_flow_pct", "unskilled_flow_pct"]
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"description": "Generates divergence signals by comparing net flow of 'Skilled' vs. 'Unskilled' cohorts, by asset and sector.",
|
|
36
|
+
"properties": {
|
|
37
|
+
"assets": {
|
|
38
|
+
"type": "object",
|
|
39
|
+
"description": "Divergence signals per asset.",
|
|
40
|
+
"patternProperties": { "^.*$": signalSchema }, // Ticker
|
|
41
|
+
"additionalProperties": signalSchema
|
|
42
|
+
},
|
|
43
|
+
"sectors": {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"description": "Divergence signals per sector.",
|
|
46
|
+
"patternProperties": { "^.*$": signalSchema }, // Sector
|
|
47
|
+
"additionalProperties": signalSchema
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"required": ["assets", "sectors"]
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Statically declare dependencies.
|
|
56
|
+
*/
|
|
57
|
+
static getDependencies() {
|
|
58
|
+
return [
|
|
59
|
+
'gem_skilled-cohort-flow', // Pass 2
|
|
60
|
+
'gem_unskilled-cohort-flow' // Pass 2
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
process() {
|
|
65
|
+
// No-op
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
_calculateDivergence(skilledFlow, unskilledFlow) {
|
|
69
|
+
const result = {};
|
|
70
|
+
if (!skilledFlow || !unskilledFlow) {
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const allKeys = new Set([...Object.keys(skilledFlow), ...Object.keys(unskilledFlow)]);
|
|
75
|
+
const THRESHOLD = 1.0; // Min flow % to register as a signal
|
|
76
|
+
|
|
77
|
+
for (const key of allKeys) {
|
|
78
|
+
const sFlow = skilledFlow[key]?.net_flow_percentage || 0;
|
|
79
|
+
const dFlow = unskilledFlow[key]?.net_flow_percentage || 0;
|
|
80
|
+
|
|
81
|
+
let status = 'Neutral';
|
|
82
|
+
|
|
83
|
+
// Both buying
|
|
84
|
+
if (sFlow > THRESHOLD && dFlow > THRESHOLD) {
|
|
85
|
+
status = 'Confirmation (Buy)';
|
|
86
|
+
}
|
|
87
|
+
// Both selling
|
|
88
|
+
else if (sFlow < -THRESHOLD && dFlow < -THRESHOLD) {
|
|
89
|
+
status = 'Confirmation (Sell)';
|
|
90
|
+
}
|
|
91
|
+
// Skilled buying, Unskilled selling
|
|
92
|
+
else if (sFlow > THRESHOLD && dFlow < -THRESHOLD) {
|
|
93
|
+
status = 'Capitulation'; // Skilled buying the dip from unskilled
|
|
94
|
+
}
|
|
95
|
+
// Skilled selling, Unskilled buying
|
|
96
|
+
else if (sFlow < -THRESHOLD && dFlow > THRESHOLD) {
|
|
97
|
+
status = 'Euphoria'; // Skilled selling into unskilled fomo
|
|
98
|
+
}
|
|
99
|
+
// Skilled buying, Unskilled neutral
|
|
100
|
+
else if (sFlow > THRESHOLD && Math.abs(dFlow) < THRESHOLD) {
|
|
101
|
+
status = 'Divergence (Skilled Buy)';
|
|
102
|
+
}
|
|
103
|
+
// Skilled selling, Unskilled neutral
|
|
104
|
+
else if (sFlow < -THRESHOLD && Math.abs(dFlow) < THRESHOLD) {
|
|
105
|
+
status = 'Divergence (Skilled Sell)';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
result[key] = {
|
|
109
|
+
status: status,
|
|
110
|
+
skilled_flow_pct: sFlow,
|
|
111
|
+
unskilled_flow_pct: dFlow
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* This is a 'meta' calculation.
|
|
119
|
+
* @param {object} fetchedDependencies - Results from Pass 2.
|
|
120
|
+
*/
|
|
121
|
+
getResult(fetchedDependencies) {
|
|
122
|
+
const skilledFlowData = fetchedDependencies['skilled-cohort-flow'];
|
|
123
|
+
const unskilledFlowData = fetchedDependencies['unskilled-cohort-flow'];
|
|
124
|
+
|
|
125
|
+
const assetResult = this._calculateDivergence(skilledFlowData?.assets, unskilledFlowData?.assets);
|
|
126
|
+
const sectorResult = this._calculateDivergence(skilledFlowData?.sectors, unskilledFlowData?.sectors);
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
assets: assetResult,
|
|
130
|
+
sectors: sectorResult
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
reset() {
|
|
135
|
+
// No state
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = SkilledUnskilledDivergence;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Calculation (Pass 4) for the "Quant-Skill Alpha Signal".
|
|
3
|
+
*
|
|
4
|
+
* This metric synthesizes multiple Pass 1, 2, and 3 signals into a
|
|
5
|
+
* single, actionable "raw score" for each asset. It is designed to
|
|
6
|
+
* be the final, tradable signal from this computation branch.
|
|
7
|
+
*
|
|
8
|
+
* It weights:
|
|
9
|
+
* 1. Skilled/Unskilled Divergence (Pass 3)
|
|
10
|
+
* 2. Unskilled "FOMO" (from Cohort Momentum, Pass 3)
|
|
11
|
+
* 3. Platform vs. Sample Divergence (Pass 1)
|
|
12
|
+
* 4. Social Media Sentiment (Pass 1)
|
|
13
|
+
*/
|
|
14
|
+
class QuantSkillAlphaSignal {
|
|
15
|
+
constructor() {
|
|
16
|
+
// Define the weights for the model.
|
|
17
|
+
// These would be optimized via backtesting.
|
|
18
|
+
this.W_DIVERGENCE = 0.40; // Skilled vs Unskilled
|
|
19
|
+
this.W_FOMO = 0.30; // Unskilled Momentum (faded)
|
|
20
|
+
this.W_PLATFORM = 0.15; // Sample vs Platform
|
|
21
|
+
this.W_SOCIAL = 0.15; // Social Sentiment
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Defines the output schema for this calculation.
|
|
26
|
+
* @returns {object} JSON Schema object
|
|
27
|
+
*/
|
|
28
|
+
static getSchema() {
|
|
29
|
+
const tickerSchema = {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"properties": {
|
|
32
|
+
"raw_alpha_score": {
|
|
33
|
+
"type": "number",
|
|
34
|
+
"description": "The final weighted signal score. > 0 is bullish, < 0 is bearish."
|
|
35
|
+
},
|
|
36
|
+
"signal_status": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"description": "A human-readable signal (e.g., 'Strong Buy', 'Neutral')."
|
|
39
|
+
},
|
|
40
|
+
"component_divergence_score": { "type": "number" },
|
|
41
|
+
"component_unskilled_fomo_score": { "type": "number" },
|
|
42
|
+
"component_platform_divergence_score": { "type": "number" },
|
|
43
|
+
"component_social_sentiment_score": { "type": "number" }
|
|
44
|
+
},
|
|
45
|
+
"required": ["raw_alpha_score", "signal_status"]
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"description": "A final, weighted alpha signal combining skill divergence, momentum, and sentiment.",
|
|
51
|
+
"patternProperties": {
|
|
52
|
+
"^.*$": tickerSchema // Ticker
|
|
53
|
+
},
|
|
54
|
+
"additionalProperties": tickerSchema
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Statically declare dependencies.
|
|
60
|
+
*/
|
|
61
|
+
static getDependencies() {
|
|
62
|
+
return [
|
|
63
|
+
'gem_skilled-unskilled-divergence', // Pass 3
|
|
64
|
+
'gem_cohort-momentum-state', // Pass 3
|
|
65
|
+
'gem_platform-conviction-divergence', // Pass 1
|
|
66
|
+
'gem_social_sentiment_aggregation' // Pass 1
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
process() {
|
|
71
|
+
// No-op
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* This is a 'meta' calculation.
|
|
76
|
+
* @param {object} fetchedDependencies - Results from previous passes.
|
|
77
|
+
*/
|
|
78
|
+
getResult(fetchedDependencies) {
|
|
79
|
+
const divergenceData = fetchedDependencies['skilled-unskilled-divergence']?.assets;
|
|
80
|
+
const momentumData = fetchedDependencies['cohort-momentum-state'];
|
|
81
|
+
const platformData = fetchedDependencies['platform-conviction-divergence'];
|
|
82
|
+
const socialData = fetchedDependencies['social_sentiment_aggregation']?.per_ticker;
|
|
83
|
+
|
|
84
|
+
if (!divergenceData || !momentumData || !platformData || !socialData) {
|
|
85
|
+
return {}; // Missing one or more key dependencies
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const result = {};
|
|
89
|
+
const allTickers = new Set([
|
|
90
|
+
...Object.keys(divergenceData),
|
|
91
|
+
...Object.keys(momentumData),
|
|
92
|
+
...Object.keys(platformData),
|
|
93
|
+
...Object.keys(socialData)
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
for (const ticker of allTickers) {
|
|
97
|
+
// 1. Get Divergence Signal (Buy = +1, Sell = -1)
|
|
98
|
+
const divStatus = divergenceData[ticker]?.status;
|
|
99
|
+
const divScore = divStatus === 'Capitulation' ? 1.0 : (divStatus === 'Euphoria' ? -1.0 : 0);
|
|
100
|
+
|
|
101
|
+
// 2. Get Unskilled FOMO Signal (We fade this, so we use -1)
|
|
102
|
+
// 'unskilled_momentum_score' is high positive when they buy a rally
|
|
103
|
+
const fomoScore = momentumData[ticker]?.unskilled_momentum_score || 0;
|
|
104
|
+
|
|
105
|
+
// 3. Get Platform Divergence Signal
|
|
106
|
+
// 'divergence' is positive when our sample is more bullish than the platform
|
|
107
|
+
const platformScore = platformData[ticker]?.divergence || 0;
|
|
108
|
+
|
|
109
|
+
// 4. Get Social Sentiment Signal
|
|
110
|
+
// 'net_sentiment_pct' is positive when social is bullish
|
|
111
|
+
const socialScore = socialData[ticker]?.net_sentiment_pct || 0;
|
|
112
|
+
|
|
113
|
+
// Normalize scores to a similar range (approx -100 to 100)
|
|
114
|
+
// (This is a simple normalization; a z-score would be more robust)
|
|
115
|
+
const s_div = divScore * 100.0;
|
|
116
|
+
const s_fomo = -1 * fomoScore; // Fading the signal
|
|
117
|
+
const s_plat = platformScore; // Already 0-100
|
|
118
|
+
const s_soc = socialScore; // Already 0-100
|
|
119
|
+
|
|
120
|
+
// Calculate final weighted score
|
|
121
|
+
const raw_alpha_score =
|
|
122
|
+
(s_div * this.W_DIVERGENCE) +
|
|
123
|
+
(s_fomo * this.W_FOMO) +
|
|
124
|
+
(s_plat * this.W_PLATFORM) +
|
|
125
|
+
(s_soc * this.W_SOCIAL);
|
|
126
|
+
|
|
127
|
+
// Determine human-readable status
|
|
128
|
+
let status = 'Neutral';
|
|
129
|
+
if (raw_alpha_score > 30) status = 'Strong Buy';
|
|
130
|
+
else if (raw_alpha_score > 10) status = 'Buy';
|
|
131
|
+
else if (raw_alpha_score < -30) status = 'Strong Sell';
|
|
132
|
+
else if (raw_alpha_score < -10) status = 'Sell';
|
|
133
|
+
|
|
134
|
+
result[ticker] = {
|
|
135
|
+
raw_alpha_score: raw_alpha_score,
|
|
136
|
+
signal_status: status,
|
|
137
|
+
component_divergence_score: s_div,
|
|
138
|
+
component_unskilled_fomo_score: s_fomo,
|
|
139
|
+
component_platform_divergence_score: s_plat,
|
|
140
|
+
component_social_sentiment_score: s_soc
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
reset() {
|
|
148
|
+
// No state
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = QuantSkillAlphaSignal;
|
|
@@ -21,8 +21,10 @@ class SocialTopicDriverIndex {
|
|
|
21
21
|
"properties": {
|
|
22
22
|
"topic": { "type": "string" },
|
|
23
23
|
"driver_score": { "type": "number" },
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
// These fields are from an older version but kept for schema
|
|
25
|
+
// compatibility. They will be null in the corrected logic.
|
|
26
|
+
"correlation_flow_30d": { "type": ["number", "null"] },
|
|
27
|
+
"correlation_price_30d": { "type": ["number", "null"] }
|
|
26
28
|
},
|
|
27
29
|
"required": ["topic", "driver_score"]
|
|
28
30
|
};
|
|
@@ -48,6 +50,7 @@ class SocialTopicDriverIndex {
|
|
|
48
50
|
|
|
49
51
|
/**
|
|
50
52
|
* Statically declare dependencies.
|
|
53
|
+
* (This was already correct)
|
|
51
54
|
*/
|
|
52
55
|
static getDependencies() {
|
|
53
56
|
return [
|
|
@@ -59,6 +62,13 @@ class SocialTopicDriverIndex {
|
|
|
59
62
|
// No-op
|
|
60
63
|
}
|
|
61
64
|
|
|
65
|
+
/**
|
|
66
|
+
* --- LOGIC FIXED ---
|
|
67
|
+
* This function is rewritten to correctly consume the output of
|
|
68
|
+
* 'social-topic-predictive-potential'. It aggregates the
|
|
69
|
+
* 'predictivePotential' score for each topic across *all* tickers
|
|
70
|
+
* to create a global driver score.
|
|
71
|
+
*/
|
|
62
72
|
getResult(fetchedDependencies) {
|
|
63
73
|
const potentialData = fetchedDependencies['social-topic-predictive-potential'];
|
|
64
74
|
|
|
@@ -67,23 +77,60 @@ class SocialTopicDriverIndex {
|
|
|
67
77
|
all_topics: []
|
|
68
78
|
};
|
|
69
79
|
|
|
70
|
-
|
|
80
|
+
// The dependency returns a nested object. We need 'daily_topic_signals'.
|
|
81
|
+
const dailyTopicSignals = potentialData?.daily_topic_signals;
|
|
82
|
+
|
|
83
|
+
if (!dailyTopicSignals || Object.keys(dailyTopicSignals).length === 0) {
|
|
71
84
|
return defaults;
|
|
72
85
|
}
|
|
73
86
|
|
|
87
|
+
// Use a Map to aggregate scores for each topic
|
|
88
|
+
const topicAggregator = new Map();
|
|
89
|
+
|
|
90
|
+
// Iterate over each TICKER (e.g., 'AAPL', 'TSLA') in the signals
|
|
91
|
+
for (const tickerData of Object.values(dailyTopicSignals)) {
|
|
92
|
+
|
|
93
|
+
// Combine bullish and bearish topics for that ticker
|
|
94
|
+
const allTickerTopics = [
|
|
95
|
+
...(tickerData.topPredictiveBullishTopics || []),
|
|
96
|
+
...(tickerData.topPredictiveBearishTopics || [])
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
// Iterate over the topics *for that ticker*
|
|
100
|
+
for (const topicData of allTickerTopics) {
|
|
101
|
+
const topicName = topicData.topic;
|
|
102
|
+
|
|
103
|
+
// Use the 'predictivePotential' score calculated by the dependency
|
|
104
|
+
const score = topicData.predictivePotential || 0;
|
|
105
|
+
|
|
106
|
+
if (!topicAggregator.has(topicName)) {
|
|
107
|
+
topicAggregator.set(topicName, { totalScore: 0, count: 0 });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const agg = topicAggregator.get(topicName);
|
|
111
|
+
agg.totalScore += score;
|
|
112
|
+
agg.count += 1;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
74
116
|
const allTopics = [];
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
117
|
+
// Now, create the final ranked list
|
|
118
|
+
for (const [topic, data] of topicAggregator.entries()) {
|
|
119
|
+
if (data.count === 0) continue;
|
|
120
|
+
|
|
121
|
+
// Calculate the average driver score across all tickers
|
|
122
|
+
const avgScore = data.totalScore / data.count;
|
|
78
123
|
|
|
79
124
|
allTopics.push({
|
|
80
125
|
topic: topic,
|
|
81
|
-
driver_score:
|
|
82
|
-
|
|
83
|
-
|
|
126
|
+
driver_score: avgScore,
|
|
127
|
+
// Set old/incompatible fields to null to match schema
|
|
128
|
+
correlation_flow_30d: null,
|
|
129
|
+
correlation_price_30d: null
|
|
84
130
|
});
|
|
85
131
|
}
|
|
86
132
|
|
|
133
|
+
// Sort by the new, correct driver_score
|
|
87
134
|
allTopics.sort((a, b) => b.driver_score - a.driver_score);
|
|
88
135
|
|
|
89
136
|
return {
|
|
@@ -152,7 +152,8 @@ function _findPriceForward(instrumentId, dateStr, priceMap) {
|
|
|
152
152
|
class SocialTopicPredictivePotentialIndex {
|
|
153
153
|
|
|
154
154
|
static getDependencies() {
|
|
155
|
-
|
|
155
|
+
// --- FIX 1: Changed from 'social-topic-driver-index' to break the cycle ---
|
|
156
|
+
return ['social-topic-sentiment-matrix'];
|
|
156
157
|
}
|
|
157
158
|
|
|
158
159
|
constructor() {
|
|
@@ -195,10 +196,13 @@ class SocialTopicPredictivePotentialIndex {
|
|
|
195
196
|
// pLimit is not in calculationUtils by default, so we'll use our own
|
|
196
197
|
// If it were, we'd use: this.pLimit = calculationUtils.pLimit(MAX_CONCURRENT_TRANSACTIONS);
|
|
197
198
|
await this._loadDependencies(calculationUtils);
|
|
198
|
-
|
|
199
|
+
|
|
200
|
+
// --- FIX 2: Read from the correct dependency ---
|
|
201
|
+
const todaySignals = fetchedDependencies['social-topic-sentiment-matrix'];
|
|
199
202
|
|
|
200
203
|
if (!todaySignals || Object.keys(todaySignals).length === 0) {
|
|
201
|
-
|
|
204
|
+
// --- FIX 2.1: Updated log message ---
|
|
205
|
+
logger.log('WARN', `[SocialTopicPredictive] Missing or empty dependency 'social-topic-sentiment-matrix' for ${dateStr}. Skipping.`);
|
|
202
206
|
return null;
|
|
203
207
|
}
|
|
204
208
|
|
|
@@ -264,6 +268,8 @@ class SocialTopicPredictivePotentialIndex {
|
|
|
264
268
|
this._updateForwardReturns(state, instrumentId, dateStr, todayPrice, this.priceMap);
|
|
265
269
|
|
|
266
270
|
// --- 4c. Add New Signals (Factored Helper) ---
|
|
271
|
+
// We assume 'todaySignal' (from social-topic-sentiment-matrix)
|
|
272
|
+
// has an 'allDrivers' property.
|
|
267
273
|
this._addNewSignals(state, todaySignal.allDrivers || [], dateStr);
|
|
268
274
|
|
|
269
275
|
// --- 4d. Recalculate Correlations (Factored Helper) ---
|