aiden-shared-calculations-unified 1.0.109 → 1.0.110
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/insights-daily-bought-vs-sold-count.js +20 -15
- package/calculations/core/insights-daily-ownership-delta.js +12 -10
- package/calculations/core/instrument-price-change-1d.js +16 -31
- package/calculations/core/instrument-price-momentum-20d.js +16 -26
- package/calculations/core/ownership-vs-performance-ytd.js +15 -52
- package/calculations/core/ownership-vs-volatility.js +27 -36
- package/calculations/core/platform-daily-bought-vs-sold-count.js +28 -26
- package/calculations/core/platform-daily-ownership-delta.js +28 -31
- package/calculations/core/price-metrics.js +15 -54
- package/calculations/core/short-interest-growth.js +6 -13
- package/calculations/core/trending-ownership-momentum.js +16 -28
- package/calculations/gauss/cohort-capital-flow.js +34 -71
- package/calculations/gauss/cohort-definer.js +61 -142
- package/calculations/gem/cohort-momentum-state.js +27 -77
- package/calculations/gem/skilled-cohort-flow.js +36 -114
- package/calculations/gem/unskilled-cohort-flow.js +36 -112
- package/calculations/ghost-book/retail-gamma-exposure.js +14 -61
- package/calculations/helix/herd-consensus-score.js +27 -90
- package/calculations/helix/winner-loser-flow.js +21 -90
- package/calculations/predicative-alpha/cognitive-dissonance.js +25 -91
- package/calculations/predicative-alpha/diamond-hand-fracture.js +17 -72
- package/calculations/predicative-alpha/mimetic-latency.js +21 -100
- package/package.json +1 -1
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Calculation (Pass 1) for ownership delta (users added/lost per asset).
|
|
3
|
-
* REFACTORED: Tracks net change in number of owners. ---> TODO IN TEST ENVIRONMENT THE VALUE OF NUMBER OF CLOSURES ALWAYS RETURNS 0, IS THIS A COMPUTATION BUG OR A TEST HARNESS BUG?
|
|
4
|
-
*/
|
|
5
1
|
class PlatformDailyOwnershipDelta {
|
|
6
2
|
constructor() {
|
|
7
|
-
this.assetChanges = new Map();
|
|
3
|
+
this.assetChanges = new Map();
|
|
8
4
|
this.tickerMap = null;
|
|
9
5
|
}
|
|
10
6
|
|
|
@@ -26,16 +22,17 @@ class PlatformDailyOwnershipDelta {
|
|
|
26
22
|
"properties": {
|
|
27
23
|
"owners_added": { "type": "number" },
|
|
28
24
|
"owners_removed": { "type": "number" },
|
|
29
|
-
"net_ownership_change": { "type": "number" }
|
|
25
|
+
"net_ownership_change": { "type": "number" },
|
|
26
|
+
"total_owners_today": { "type": "number" }
|
|
30
27
|
},
|
|
31
|
-
"required": ["owners_added", "owners_removed", "net_ownership_change"]
|
|
28
|
+
"required": ["owners_added", "owners_removed", "net_ownership_change", "total_owners_today"]
|
|
32
29
|
};
|
|
33
30
|
return { "type": "object", "patternProperties": { "^.*$": tickerSchema } };
|
|
34
31
|
}
|
|
35
32
|
|
|
36
33
|
_initAsset(instId) {
|
|
37
34
|
if (!this.assetChanges.has(instId)) {
|
|
38
|
-
this.assetChanges.set(instId, { added: 0, removed: 0 });
|
|
35
|
+
this.assetChanges.set(instId, { added: 0, removed: 0, total: 0 });
|
|
39
36
|
}
|
|
40
37
|
}
|
|
41
38
|
|
|
@@ -53,28 +50,29 @@ class PlatformDailyOwnershipDelta {
|
|
|
53
50
|
const { mappings, user } = context;
|
|
54
51
|
if (!this.tickerMap) this.tickerMap = mappings.instrumentToTicker;
|
|
55
52
|
|
|
56
|
-
// If either portfolio is missing, we can't calculate delta for this user
|
|
57
|
-
if (!user.portfolio.today || !user.portfolio.yesterday) return;
|
|
58
|
-
|
|
59
53
|
const todayPositions = extract.getPositions(user.portfolio.today, user.type);
|
|
60
|
-
const yesterdayPositions = extract.getPositions(user.portfolio.yesterday, user.type);
|
|
61
|
-
|
|
62
54
|
const tSet = this._getOwnedInstruments(todayPositions, extract);
|
|
63
|
-
const ySet = this._getOwnedInstruments(yesterdayPositions, extract);
|
|
64
55
|
|
|
65
|
-
//
|
|
56
|
+
// Establish current total for Baseline
|
|
66
57
|
for (const instId of tSet) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
this.assetChanges.get(instId).added++;
|
|
70
|
-
}
|
|
58
|
+
this._initAsset(instId);
|
|
59
|
+
this.assetChanges.get(instId).total++;
|
|
71
60
|
}
|
|
72
61
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
62
|
+
if (user.portfolio.yesterday) {
|
|
63
|
+
const yesterdayPositions = extract.getPositions(user.portfolio.yesterday, user.type);
|
|
64
|
+
const ySet = this._getOwnedInstruments(yesterdayPositions, extract);
|
|
65
|
+
for (const instId of tSet) {
|
|
66
|
+
if (!ySet.has(instId)) {
|
|
67
|
+
this._initAsset(instId);
|
|
68
|
+
this.assetChanges.get(instId).added++;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for (const instId of ySet) {
|
|
72
|
+
if (!tSet.has(instId)) {
|
|
73
|
+
this._initAsset(instId);
|
|
74
|
+
this.assetChanges.get(instId).removed++;
|
|
75
|
+
}
|
|
78
76
|
}
|
|
79
77
|
}
|
|
80
78
|
}
|
|
@@ -84,13 +82,12 @@ class PlatformDailyOwnershipDelta {
|
|
|
84
82
|
const result = {};
|
|
85
83
|
for (const [instId, data] of this.assetChanges.entries()) {
|
|
86
84
|
const ticker = this.tickerMap[instId] || `id_${instId}`;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
85
|
+
result[ticker] = {
|
|
86
|
+
owners_added: data.added,
|
|
87
|
+
owners_removed: data.removed,
|
|
88
|
+
net_ownership_change: data.added - data.removed,
|
|
89
|
+
total_owners_today: data.total
|
|
90
|
+
};
|
|
94
91
|
}
|
|
95
92
|
return result;
|
|
96
93
|
}
|
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Calculation (Pass 1 - Meta) for historical price metrics.
|
|
3
|
-
* FIXED: Supports Sharded/Batched Execution.
|
|
4
|
-
* MOVED: Sector aggregation logic moved to getResult() to handle sharding correctly.
|
|
5
|
-
*/
|
|
6
1
|
const RANGES = [7, 30, 90, 365];
|
|
7
2
|
const TRADING_DAYS_PER_YEAR = 252;
|
|
8
3
|
|
|
9
4
|
class CorePriceMetrics {
|
|
10
5
|
constructor() {
|
|
11
6
|
this.result = { by_instrument: {}, by_sector: {} };
|
|
12
|
-
// We persist mappings here because we process shard-by-shard
|
|
13
7
|
this.mappings = null;
|
|
14
8
|
}
|
|
15
9
|
|
|
@@ -49,13 +43,11 @@ class CorePriceMetrics {
|
|
|
49
43
|
const prices = [];
|
|
50
44
|
let currentDate = new Date(endDateStr + 'T00:00:00Z');
|
|
51
45
|
let lastPrice = null;
|
|
52
|
-
|
|
53
46
|
for (let i = 0; i < numDays; i++) {
|
|
54
47
|
const targetDateStr = currentDate.toISOString().slice(0, 10);
|
|
55
48
|
let price = this._findPriceOnOrBefore(priceHistoryObj, targetDateStr);
|
|
56
49
|
if (price === null) price = lastPrice;
|
|
57
50
|
else lastPrice = price;
|
|
58
|
-
|
|
59
51
|
if (price !== null) prices.push(price);
|
|
60
52
|
currentDate.setUTCDate(currentDate.getUTCDate() - 1);
|
|
61
53
|
}
|
|
@@ -63,9 +55,9 @@ class CorePriceMetrics {
|
|
|
63
55
|
}
|
|
64
56
|
|
|
65
57
|
_calculateStats(priceArray) {
|
|
66
|
-
|
|
58
|
+
const currentPrice = priceArray[priceArray.length - 1] || null;
|
|
59
|
+
if (priceArray.length < 2) return { stdDev: null, mean: null, drawdown: null, currentPrice };
|
|
67
60
|
|
|
68
|
-
// Drawdown
|
|
69
61
|
let maxDrawdown = 0, peak = -Infinity;
|
|
70
62
|
for (const price of priceArray) {
|
|
71
63
|
if (price > peak) peak = price;
|
|
@@ -74,8 +66,6 @@ class CorePriceMetrics {
|
|
|
74
66
|
if (dd < maxDrawdown) maxDrawdown = dd;
|
|
75
67
|
}
|
|
76
68
|
}
|
|
77
|
-
|
|
78
|
-
// Returns & StdDev
|
|
79
69
|
const returns = [];
|
|
80
70
|
for (let i = 1; i < priceArray.length; i++) {
|
|
81
71
|
const prev = priceArray[i - 1];
|
|
@@ -84,78 +74,53 @@ class CorePriceMetrics {
|
|
|
84
74
|
}
|
|
85
75
|
const mean = returns.reduce((a, b) => a + b, 0) / returns.length;
|
|
86
76
|
const variance = returns.reduce((a, b) => a + (b - mean) ** 2, 0) / (returns.length - 1);
|
|
87
|
-
|
|
88
|
-
return { stdDev: Math.sqrt(variance), mean, drawdown: maxDrawdown };
|
|
77
|
+
return { stdDev: Math.sqrt(variance), mean, drawdown: maxDrawdown, currentPrice };
|
|
89
78
|
}
|
|
90
79
|
|
|
91
80
|
process(context) {
|
|
92
81
|
const { mappings, prices, date } = context;
|
|
93
82
|
const { instrumentToTicker } = mappings;
|
|
94
|
-
|
|
95
|
-
// Save mappings for the final aggregation step
|
|
96
83
|
if (!this.mappings) this.mappings = mappings;
|
|
97
|
-
|
|
98
84
|
const priceData = prices?.history;
|
|
99
85
|
const todayDateStr = date.today;
|
|
86
|
+
if (!priceData || !todayDateStr) return;
|
|
100
87
|
|
|
101
|
-
if (!priceData || !todayDateStr) {
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Iterate over THIS SHARD'S data
|
|
106
88
|
for (const p of Object.values(priceData)) {
|
|
107
89
|
const ticker = instrumentToTicker[p.instrumentId];
|
|
108
90
|
if (!ticker) continue;
|
|
109
|
-
|
|
110
|
-
const metrics = {};
|
|
91
|
+
const metrics = { baseline_price: null };
|
|
111
92
|
for (const range of RANGES) {
|
|
112
93
|
const priceArray = this._getHistoricalPriceArray(p, todayDateStr, range + 1);
|
|
113
94
|
const stats = this._calculateStats(priceArray);
|
|
114
|
-
|
|
115
|
-
metrics[`stdev_${range}d`] = stats.stdDev;
|
|
116
|
-
metrics[`max_drawdown_${range}d`] = stats.drawdown;
|
|
117
|
-
metrics[`volatility_annualized_${range}d`] = stats.stdDev * Math.sqrt(TRADING_DAYS_PER_YEAR);
|
|
118
|
-
metrics[`sharpe_ratio_${range}d`] = stats.stdDev > 0
|
|
119
|
-
? (stats.mean / stats.stdDev) * Math.sqrt(TRADING_DAYS_PER_YEAR)
|
|
120
|
-
: 0;
|
|
95
|
+
metrics.baseline_price = stats.currentPrice;
|
|
96
|
+
metrics[`stdev_${range}d`] = (stats.stdDev !== null && isFinite(stats.stdDev)) ? stats.stdDev : null;
|
|
97
|
+
metrics[`max_drawdown_${range}d`] = (stats.drawdown !== null && isFinite(stats.drawdown)) ? stats.drawdown : null;
|
|
98
|
+
metrics[`volatility_annualized_${range}d`] = (stats.stdDev !== null) ? stats.stdDev * Math.sqrt(TRADING_DAYS_PER_YEAR) : null;
|
|
99
|
+
metrics[`sharpe_ratio_${range}d`] = (stats.stdDev > 0) ? (stats.mean / stats.stdDev) * Math.sqrt(TRADING_DAYS_PER_YEAR) : null;
|
|
121
100
|
}
|
|
122
|
-
// Accumulate into by_instrument
|
|
123
101
|
this.result.by_instrument[ticker] = metrics;
|
|
124
102
|
}
|
|
125
103
|
}
|
|
126
104
|
|
|
127
105
|
async getResult() {
|
|
128
|
-
// Perform Sector Aggregation HERE (after all shards are processed)
|
|
129
106
|
const by_instrument = this.result.by_instrument;
|
|
130
107
|
const instrumentToSector = this.mappings?.instrumentToSector || {};
|
|
131
108
|
const instrumentToTicker = this.mappings?.instrumentToTicker || {};
|
|
132
|
-
|
|
133
|
-
// Reverse map ticker -> instrumentId
|
|
134
109
|
const tickerToInstrument = {};
|
|
135
|
-
for(const [id, tick] of Object.entries(instrumentToTicker))
|
|
136
|
-
tickerToInstrument[tick] = id;
|
|
137
|
-
}
|
|
110
|
+
for(const [id, tick] of Object.entries(instrumentToTicker)) tickerToInstrument[tick] = id;
|
|
138
111
|
|
|
139
112
|
const sectorAggs = {};
|
|
140
113
|
for (const ticker in by_instrument) {
|
|
141
114
|
const instId = tickerToInstrument[ticker];
|
|
142
115
|
const sector = instrumentToSector[instId] || "Unknown";
|
|
143
|
-
|
|
144
116
|
if (!sectorAggs[sector]) sectorAggs[sector] = { metrics: {}, counts: {} };
|
|
145
|
-
|
|
146
117
|
const data = by_instrument[ticker];
|
|
147
118
|
for (const key in data) {
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
if (data[key] !== null) {
|
|
153
|
-
sectorAggs[sector].metrics[key] += data[key];
|
|
154
|
-
sectorAggs[sector].counts[key]++;
|
|
155
|
-
}
|
|
119
|
+
if (key === 'baseline_price') continue;
|
|
120
|
+
if (!sectorAggs[sector].metrics[key]) { sectorAggs[sector].metrics[key] = 0; sectorAggs[sector].counts[key] = 0; }
|
|
121
|
+
if (data[key] !== null) { sectorAggs[sector].metrics[key] += data[key]; sectorAggs[sector].counts[key]++; }
|
|
156
122
|
}
|
|
157
123
|
}
|
|
158
|
-
|
|
159
124
|
const by_sector = {};
|
|
160
125
|
for (const sector in sectorAggs) {
|
|
161
126
|
by_sector[sector] = {};
|
|
@@ -164,14 +129,10 @@ class CorePriceMetrics {
|
|
|
164
129
|
by_sector[sector][`average_${key}`] = count > 0 ? sectorAggs[sector].metrics[key] / count : null;
|
|
165
130
|
}
|
|
166
131
|
}
|
|
167
|
-
|
|
168
132
|
this.result.by_sector = by_sector;
|
|
169
133
|
return this.result;
|
|
170
134
|
}
|
|
171
135
|
|
|
172
|
-
reset() {
|
|
173
|
-
this.result = { by_instrument: {}, by_sector: {} };
|
|
174
|
-
this.mappings = null;
|
|
175
|
-
}
|
|
136
|
+
reset() { this.result = { by_instrument: {}, by_sector: {} }; this.mappings = null; }
|
|
176
137
|
}
|
|
177
138
|
module.exports = CorePriceMetrics;
|
|
@@ -19,7 +19,7 @@ class ShortInterestGrowth {
|
|
|
19
19
|
"TICKER": {
|
|
20
20
|
"shortCount": 0,
|
|
21
21
|
"shortSparkline": [0, 0, 0, 0, 0, 0, 0],
|
|
22
|
-
"growth7d":
|
|
22
|
+
"growth7d": { "type": ["number", "null"] }
|
|
23
23
|
}
|
|
24
24
|
};
|
|
25
25
|
}
|
|
@@ -27,7 +27,6 @@ class ShortInterestGrowth {
|
|
|
27
27
|
async process(context) {
|
|
28
28
|
const { insights: insightsHelper } = context.math;
|
|
29
29
|
const { previousComputed, mappings } = context;
|
|
30
|
-
|
|
31
30
|
const dailyInsights = insightsHelper.getInsights(context, 'today');
|
|
32
31
|
const previousState = previousComputed['short-interest-growth'] || {};
|
|
33
32
|
|
|
@@ -36,34 +35,28 @@ class ShortInterestGrowth {
|
|
|
36
35
|
const ticker = mappings.instrumentToTicker[instId];
|
|
37
36
|
if (!ticker) continue;
|
|
38
37
|
|
|
39
|
-
// Calculate Short Count: Total Owners * (Sell % / 100)
|
|
40
38
|
const shortCount = insightsHelper.getShortCount(insight);
|
|
41
|
-
|
|
42
39
|
const prevState = previousState[ticker] || { shortSparkline: [] };
|
|
43
40
|
const sparkline = [...(prevState.shortSparkline || [])];
|
|
44
|
-
|
|
45
41
|
sparkline.push(shortCount);
|
|
46
42
|
if (sparkline.length > 7) sparkline.shift();
|
|
47
43
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (sparkline.length > 1) {
|
|
44
|
+
let growth7d = null;
|
|
45
|
+
if (sparkline.length >= 7) {
|
|
51
46
|
const start = sparkline[0];
|
|
52
47
|
const end = sparkline[sparkline.length - 1];
|
|
53
|
-
if (start > 0)
|
|
54
|
-
growth7d = ((end - start) / start) * 100;
|
|
55
|
-
}
|
|
48
|
+
if (start > 0) growth7d = ((end - start) / start) * 100;
|
|
56
49
|
}
|
|
57
50
|
|
|
58
51
|
this.results[ticker] = {
|
|
59
52
|
shortCount,
|
|
60
53
|
shortSparkline: sparkline,
|
|
61
|
-
growth7d
|
|
54
|
+
growth7d: (growth7d !== null && isFinite(growth7d)) ? growth7d : null
|
|
62
55
|
};
|
|
63
56
|
}
|
|
64
57
|
}
|
|
65
58
|
|
|
66
59
|
async getResult() { return this.results; }
|
|
60
|
+
reset() { this.results = {}; }
|
|
67
61
|
}
|
|
68
|
-
|
|
69
62
|
module.exports = ShortInterestGrowth;
|
|
@@ -7,7 +7,7 @@ class TrendingOwnershipMomentum {
|
|
|
7
7
|
type: 'meta',
|
|
8
8
|
category: 'core',
|
|
9
9
|
userType: 'n/a',
|
|
10
|
-
isHistorical: true,
|
|
10
|
+
isHistorical: true,
|
|
11
11
|
rootDataDependencies: ['insights']
|
|
12
12
|
};
|
|
13
13
|
}
|
|
@@ -18,9 +18,9 @@ class TrendingOwnershipMomentum {
|
|
|
18
18
|
return {
|
|
19
19
|
"TICKER": {
|
|
20
20
|
"currentOwners": 0,
|
|
21
|
-
"sparkline": [0, 0, 0, 0, 0, 0, 0],
|
|
22
|
-
"momentumScore":
|
|
23
|
-
"trend": "string"
|
|
21
|
+
"sparkline": [0, 0, 0, 0, 0, 0, 0],
|
|
22
|
+
"momentumScore": { "type": ["number", "null"] },
|
|
23
|
+
"trend": "string"
|
|
24
24
|
}
|
|
25
25
|
};
|
|
26
26
|
}
|
|
@@ -28,12 +28,8 @@ class TrendingOwnershipMomentum {
|
|
|
28
28
|
async process(context) {
|
|
29
29
|
const { insights: insightsHelper } = context.math;
|
|
30
30
|
const { previousComputed, mappings } = context;
|
|
31
|
-
|
|
32
|
-
// 1. Get Today's Data
|
|
33
31
|
const dailyInsights = insightsHelper.getInsights(context, 'today');
|
|
34
|
-
if (!dailyInsights.length) return;
|
|
35
|
-
|
|
36
|
-
// 2. Get Yesterday's State (for rolling history)
|
|
32
|
+
if (!dailyInsights || !dailyInsights.length) return;
|
|
37
33
|
const previousState = previousComputed['trending-ownership-momentum'] || {};
|
|
38
34
|
|
|
39
35
|
for (const insight of dailyInsights) {
|
|
@@ -43,45 +39,37 @@ class TrendingOwnershipMomentum {
|
|
|
43
39
|
|
|
44
40
|
const currentCount = insightsHelper.getTotalOwners(insight);
|
|
45
41
|
const prevState = previousState[ticker] || { sparkline: [] };
|
|
46
|
-
|
|
47
|
-
// 3. Update Rolling Window (Last 7 Days)
|
|
48
|
-
// Create new array copy to avoid mutation issues
|
|
49
42
|
const sparkline = [...(prevState.sparkline || [])];
|
|
50
43
|
sparkline.push(currentCount);
|
|
51
|
-
|
|
52
|
-
// Keep only last 7 entries
|
|
53
44
|
if (sparkline.length > 7) sparkline.shift();
|
|
54
45
|
|
|
55
|
-
|
|
56
|
-
let
|
|
57
|
-
|
|
46
|
+
let momentumScore = null;
|
|
47
|
+
let trend = 'WARM_UP';
|
|
48
|
+
|
|
49
|
+
if (sparkline.length >= 7) {
|
|
58
50
|
const n = sparkline.length;
|
|
59
51
|
let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0;
|
|
60
52
|
for (let i = 0; i < n; i++) {
|
|
61
|
-
sumX += i;
|
|
62
|
-
sumY += sparkline[i];
|
|
63
|
-
sumXY += i * sparkline[i];
|
|
64
|
-
sumXX += i * i;
|
|
53
|
+
sumX += i; sumY += sparkline[i]; sumXY += i * sparkline[i]; sumXX += i * i;
|
|
65
54
|
}
|
|
66
55
|
const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
|
|
67
|
-
// Normalize slope by current count to get percentage-like momentum
|
|
68
56
|
momentumScore = currentCount > 0 ? (slope / currentCount) * 100 : 0;
|
|
57
|
+
|
|
58
|
+
trend = 'STABLE';
|
|
59
|
+
if (momentumScore > 0.5) trend = 'RISING';
|
|
60
|
+
if (momentumScore < -0.5) trend = 'FALLING';
|
|
69
61
|
}
|
|
70
62
|
|
|
71
|
-
let trend = 'STABLE';
|
|
72
|
-
if (momentumScore > 0.5) trend = 'RISING';
|
|
73
|
-
if (momentumScore < -0.5) trend = 'FALLING';
|
|
74
|
-
|
|
75
63
|
this.results[ticker] = {
|
|
76
64
|
currentOwners: currentCount,
|
|
77
65
|
sparkline,
|
|
78
|
-
momentumScore,
|
|
66
|
+
momentumScore: (momentumScore !== null && isFinite(momentumScore)) ? momentumScore : null,
|
|
79
67
|
trend
|
|
80
68
|
};
|
|
81
69
|
}
|
|
82
70
|
}
|
|
83
71
|
|
|
84
72
|
async getResult() { return this.results; }
|
|
73
|
+
reset() { this.results = {}; }
|
|
85
74
|
}
|
|
86
|
-
|
|
87
75
|
module.exports = TrendingOwnershipMomentum;
|
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview GAUSS Product Line (Pass 3)
|
|
3
|
-
* REFACTORED: Implemented getResult logic and mapping caching.
|
|
4
|
-
*/
|
|
5
1
|
class CohortCapitalFlow {
|
|
6
2
|
constructor() {
|
|
7
3
|
this.cohortFlows = new Map();
|
|
8
4
|
this.cohortMap = new Map();
|
|
9
|
-
this.tickerMap = null;
|
|
5
|
+
this.tickerMap = null;
|
|
10
6
|
this.dependenciesLoaded = false;
|
|
11
7
|
}
|
|
12
8
|
|
|
@@ -14,36 +10,26 @@ class CohortCapitalFlow {
|
|
|
14
10
|
const flowSchema = {
|
|
15
11
|
"type": "object",
|
|
16
12
|
"properties": {
|
|
17
|
-
"net_flow_percentage": { "type": "number" },
|
|
18
|
-
"net_flow_contribution": { "type": "number" }
|
|
13
|
+
"net_flow_percentage": { "type": ["number", "null"] },
|
|
14
|
+
"net_flow_contribution": { "type": ["number", "null"] },
|
|
15
|
+
"total_invested_today": { "type": "number" } // Baseline
|
|
19
16
|
},
|
|
20
|
-
"required": ["net_flow_percentage", "net_flow_contribution"]
|
|
21
|
-
};
|
|
22
|
-
return {
|
|
23
|
-
"type": "object",
|
|
24
|
-
"patternProperties": { "^.*$": { "type": "object", "patternProperties": { "^.*$": flowSchema } } }
|
|
17
|
+
"required": ["net_flow_percentage", "net_flow_contribution", "total_invested_today"]
|
|
25
18
|
};
|
|
19
|
+
return { "type": "object", "patternProperties": { "^.*$": { "type": "object", "patternProperties": { "^.*$": flowSchema } } } };
|
|
26
20
|
}
|
|
27
21
|
|
|
28
22
|
static getMetadata() {
|
|
29
|
-
return {
|
|
30
|
-
type: 'standard',
|
|
31
|
-
rootDataDependencies: ['portfolio', 'history'],
|
|
32
|
-
isHistorical: true,
|
|
33
|
-
userType: 'all',
|
|
34
|
-
category: 'gauss'
|
|
35
|
-
};
|
|
23
|
+
return { type: 'standard', rootDataDependencies: ['portfolio', 'history'], isHistorical: true, userType: 'all', category: 'gauss' };
|
|
36
24
|
}
|
|
37
25
|
|
|
38
|
-
static getDependencies() {
|
|
39
|
-
return ['cohort-definer', 'instrument-price-change-1d'];
|
|
40
|
-
}
|
|
26
|
+
static getDependencies() { return ['cohort-definer', 'instrument-price-change-1d']; }
|
|
41
27
|
|
|
42
28
|
_initFlowData(cohortName, instrumentId) {
|
|
43
29
|
if (!this.cohortFlows.has(cohortName)) this.cohortFlows.set(cohortName, new Map());
|
|
44
30
|
if (!this.cohortFlows.get(cohortName).has(instrumentId)) {
|
|
45
31
|
this.cohortFlows.get(cohortName).set(instrumentId, {
|
|
46
|
-
total_invested_yesterday: 0, total_invested_today: 0, price_change_yesterday: 0
|
|
32
|
+
total_invested_yesterday: 0, total_invested_today: 0, price_change_yesterday: 0, has_yesterday: false
|
|
47
33
|
});
|
|
48
34
|
}
|
|
49
35
|
}
|
|
@@ -62,84 +48,61 @@ class CohortCapitalFlow {
|
|
|
62
48
|
process(context) {
|
|
63
49
|
const { user, computed, mappings, math } = context;
|
|
64
50
|
const { extract } = math;
|
|
65
|
-
|
|
66
|
-
// Cache mapping for getResult phase
|
|
67
51
|
if (!this.tickerMap) this.tickerMap = mappings.instrumentToTicker;
|
|
68
|
-
|
|
69
52
|
this._loadDependencies(computed);
|
|
70
|
-
|
|
71
53
|
const cohortName = this.cohortMap.get(user.id);
|
|
72
54
|
if (!cohortName) return;
|
|
73
55
|
|
|
74
56
|
const priceChangeMap = computed['instrument-price-change-1d'];
|
|
75
|
-
if (!priceChangeMap) return;
|
|
76
|
-
|
|
77
|
-
const yPos = extract.getPositions(user.portfolio.yesterday, user.type);
|
|
78
57
|
const tPos = extract.getPositions(user.portfolio.today, user.type);
|
|
58
|
+
const yPos = user.portfolio.yesterday ? extract.getPositions(user.portfolio.yesterday, user.type) : [];
|
|
79
59
|
|
|
80
60
|
const yPosMap = new Map(yPos.map(p => [extract.getInstrumentId(p), p]));
|
|
81
61
|
const tPosMap = new Map(tPos.map(p => [extract.getInstrumentId(p), p]));
|
|
82
62
|
const allInstrumentIds = new Set([...yPosMap.keys(), ...tPosMap.keys()]);
|
|
83
63
|
|
|
84
64
|
for (const instId of allInstrumentIds) {
|
|
85
|
-
if (!instId) continue;
|
|
86
|
-
|
|
87
65
|
this._initFlowData(cohortName, instId);
|
|
88
66
|
const asset = this.cohortFlows.get(cohortName).get(instId);
|
|
89
|
-
|
|
90
|
-
const yInvested = extract.getPositionWeight(yPosMap.get(instId), user.type);
|
|
91
67
|
const tInvested = extract.getPositionWeight(tPosMap.get(instId), user.type);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
68
|
+
if (tInvested > 0) asset.total_invested_today += tInvested;
|
|
69
|
+
|
|
70
|
+
if (user.portfolio.yesterday) {
|
|
71
|
+
asset.has_yesterday = true;
|
|
72
|
+
const yInvested = extract.getPositionWeight(yPosMap.get(instId), user.type);
|
|
73
|
+
if (yInvested > 0) {
|
|
74
|
+
asset.total_invested_yesterday += yInvested;
|
|
75
|
+
const ticker = this.tickerMap[instId];
|
|
76
|
+
const yPriceChange = (ticker && priceChangeMap?.[ticker]) ? priceChangeMap[ticker].change_1d_pct : 0;
|
|
77
|
+
asset.price_change_yesterday += ((yPriceChange || 0) / 100.0) * yInvested;
|
|
78
|
+
}
|
|
103
79
|
}
|
|
104
80
|
}
|
|
105
81
|
}
|
|
106
82
|
|
|
107
83
|
async getResult() {
|
|
108
84
|
const finalResult = {};
|
|
109
|
-
|
|
110
|
-
if (!this.tickerMap) return {}; // Should be populated by process()
|
|
111
|
-
|
|
85
|
+
if (!this.tickerMap) return {};
|
|
112
86
|
for (const [cohortName, assetsMap] of this.cohortFlows.entries()) {
|
|
113
87
|
finalResult[cohortName] = {};
|
|
114
|
-
|
|
115
88
|
for (const [instId, data] of assetsMap.entries()) {
|
|
116
89
|
const ticker = this.tickerMap[instId] || `id_${instId}`;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const denominator = Math.max(1, (data.total_invested_yesterday + data.total_invested_today) / 2);
|
|
125
|
-
const netFlowPct = (netFlow / denominator) * 100;
|
|
126
|
-
|
|
90
|
+
let netFlow = null, netFlowPct = null;
|
|
91
|
+
if (data.has_yesterday) {
|
|
92
|
+
const expectedToday = data.total_invested_yesterday + data.price_change_yesterday;
|
|
93
|
+
netFlow = data.total_invested_today - expectedToday;
|
|
94
|
+
const denom = Math.max(1, (data.total_invested_yesterday + data.total_invested_today) / 2);
|
|
95
|
+
netFlowPct = (netFlow / denom) * 100;
|
|
96
|
+
}
|
|
127
97
|
finalResult[cohortName][ticker] = {
|
|
128
|
-
net_flow_percentage: netFlowPct,
|
|
129
|
-
net_flow_contribution: netFlow
|
|
98
|
+
net_flow_percentage: (netFlowPct !== null && isFinite(netFlowPct)) ? netFlowPct : null,
|
|
99
|
+
net_flow_contribution: (netFlow !== null && isFinite(netFlow)) ? netFlow : null,
|
|
100
|
+
total_invested_today: data.total_invested_today
|
|
130
101
|
};
|
|
131
102
|
}
|
|
132
103
|
}
|
|
133
|
-
|
|
134
104
|
return finalResult;
|
|
135
105
|
}
|
|
136
|
-
|
|
137
|
-
reset() {
|
|
138
|
-
this.cohortFlows.clear();
|
|
139
|
-
this.cohortMap.clear();
|
|
140
|
-
this.tickerMap = null;
|
|
141
|
-
this.dependenciesLoaded = false;
|
|
142
|
-
}
|
|
106
|
+
reset() { this.cohortFlows.clear(); this.cohortMap.clear(); this.tickerMap = null; this.dependenciesLoaded = false; }
|
|
143
107
|
}
|
|
144
|
-
|
|
145
|
-
module.exports = CohortCapitalFlow;
|
|
108
|
+
module.exports = CohortCapitalFlow;
|