aiden-shared-calculations-unified 1.0.86 → 1.0.87
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 +36 -106
- package/calculations/core/asset-position-size.js +40 -91
- package/calculations/core/average-daily-pnl-all-users.js +18 -57
- package/calculations/core/average-daily-pnl-per-sector.js +41 -88
- package/calculations/core/average-daily-pnl-per-stock.js +38 -91
- package/calculations/core/average-daily-position-pnl.js +19 -49
- package/calculations/core/holding-duration-per-asset.js +25 -127
- package/calculations/core/instrument-price-change-1d.js +30 -49
- package/calculations/core/instrument-price-momentum-20d.js +50 -60
- package/calculations/core/long-position-per-stock.js +39 -68
- package/calculations/core/overall-holding-duration.js +16 -87
- package/calculations/core/overall-profitability-ratio.js +11 -40
- package/calculations/core/platform-buy-sell-sentiment.js +41 -124
- package/calculations/core/platform-daily-bought-vs-sold-count.js +41 -99
- package/calculations/core/platform-daily-ownership-delta.js +68 -126
- package/calculations/core/platform-ownership-per-sector.js +45 -96
- package/calculations/core/platform-total-positions-held.js +20 -80
- package/calculations/core/pnl-distribution-per-stock.js +29 -135
- package/calculations/core/price-metrics.js +95 -206
- package/calculations/core/profitability-ratio-per-sector.js +34 -79
- package/calculations/core/profitability-ratio-per-stock.js +32 -88
- package/calculations/core/profitability-skew-per-stock.js +41 -94
- package/calculations/core/profitable-and-unprofitable-status.js +44 -76
- package/calculations/core/sentiment-per-stock.js +24 -77
- package/calculations/core/short-position-per-stock.js +35 -43
- package/calculations/core/social-activity-aggregation.js +26 -49
- package/calculations/core/social-asset-posts-trend.js +38 -94
- package/calculations/core/social-event-correlation.js +26 -93
- package/calculations/core/social-sentiment-aggregation.js +20 -44
- package/calculations/core/social-top-mentioned-words.js +35 -87
- package/calculations/core/social-topic-interest-evolution.js +22 -111
- package/calculations/core/social-topic-sentiment-matrix.js +38 -104
- package/calculations/core/social-word-mentions-trend.js +27 -104
- package/calculations/core/speculator-asset-sentiment.js +31 -72
- package/calculations/core/speculator-danger-zone.js +48 -84
- package/calculations/core/speculator-distance-to-stop-loss-per-leverage.js +20 -52
- package/calculations/core/speculator-distance-to-tp-per-leverage.js +23 -53
- package/calculations/core/speculator-entry-distance-to-sl-per-leverage.js +20 -50
- package/calculations/core/speculator-entry-distance-to-tp-per-leverage.js +23 -50
- package/calculations/core/speculator-leverage-per-asset.js +25 -64
- package/calculations/core/speculator-leverage-per-sector.js +27 -63
- package/calculations/core/speculator-risk-reward-ratio-per-asset.js +24 -53
- package/calculations/core/speculator-stop-loss-distance-by-sector-short-long-breakdown.js +55 -68
- package/calculations/core/speculator-stop-loss-distance-by-ticker-short-long-breakdown.js +54 -71
- package/calculations/core/speculator-stop-loss-per-asset.js +19 -44
- package/calculations/core/speculator-take-profit-per-asset.js +20 -57
- package/calculations/core/speculator-tsl-per-asset.js +17 -56
- package/calculations/core/total-long-figures.js +16 -31
- package/calculations/core/total-long-per-sector.js +39 -61
- package/calculations/core/total-short-figures.js +13 -32
- package/calculations/core/total-short-per-sector.js +39 -61
- package/calculations/core/users-processed.js +11 -46
- package/calculations/gauss/cohort-capital-flow.js +54 -173
- package/calculations/gauss/cohort-definer.js +77 -163
- package/calculations/gauss/daily-dna-filter.js +29 -83
- package/calculations/gauss/gauss-divergence-signal.js +22 -109
- package/calculations/gem/cohort-momentum-state.js +27 -72
- package/calculations/gem/cohort-skill-definition.js +36 -52
- package/calculations/gem/platform-conviction-divergence.js +18 -60
- package/calculations/gem/quant-skill-alpha-signal.js +25 -98
- package/calculations/gem/skilled-cohort-flow.js +67 -175
- package/calculations/gem/skilled-unskilled-divergence.js +18 -73
- package/calculations/gem/unskilled-cohort-flow.js +64 -172
- package/calculations/helix/helix-contrarian-signal.js +20 -114
- package/calculations/helix/herd-consensus-score.js +42 -124
- package/calculations/helix/winner-loser-flow.js +36 -118
- package/calculations/pyro/risk-appetite-index.js +33 -74
- package/calculations/pyro/squeeze-potential.js +30 -87
- package/calculations/pyro/volatility-signal.js +33 -78
- package/package.json +1 -1
|
@@ -1,156 +1,73 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Calculation (Pass 1) for
|
|
3
|
-
*
|
|
4
|
-
* REFACTOR: This is now a 'type: "meta"' calculation. It runs ONCE.
|
|
5
|
-
* It reads the pre-aggregated 'insights' data source and sums the
|
|
6
|
-
* 'buy' and 'sell' counts from all instruments.
|
|
2
|
+
* @fileoverview Calculation (Pass 1) for platform-wide Buy/Sell ratio.
|
|
3
|
+
* REFACTORED: Uses exposure weights.
|
|
7
4
|
*/
|
|
8
|
-
class
|
|
9
|
-
|
|
10
|
-
// --- STANDARD 2: ADDED ---
|
|
5
|
+
class PlatformBuySellSentiment {
|
|
11
6
|
constructor() {
|
|
12
|
-
this.
|
|
7
|
+
this.longWeight = 0;
|
|
8
|
+
this.shortWeight = 0;
|
|
9
|
+
this.longCount = 0;
|
|
10
|
+
this.shortCount = 0;
|
|
13
11
|
}
|
|
14
12
|
|
|
15
|
-
/**
|
|
16
|
-
* Statically defines all metadata for the manifest builder.
|
|
17
|
-
*/
|
|
18
13
|
static getMetadata() {
|
|
19
14
|
return {
|
|
20
|
-
type: '
|
|
21
|
-
rootDataDependencies: ['
|
|
15
|
+
type: 'standard',
|
|
16
|
+
rootDataDependencies: ['portfolio'],
|
|
22
17
|
isHistorical: false,
|
|
23
|
-
userType: '
|
|
18
|
+
userType: 'all',
|
|
24
19
|
category: 'core_sentiment'
|
|
25
20
|
};
|
|
26
21
|
}
|
|
27
22
|
|
|
28
|
-
|
|
29
|
-
* Statically declare dependencies.
|
|
30
|
-
*/
|
|
31
|
-
static getDependencies() {
|
|
32
|
-
return [];
|
|
33
|
-
}
|
|
23
|
+
static getDependencies() { return []; }
|
|
34
24
|
|
|
35
|
-
/**
|
|
36
|
-
* Defines the output schema for this calculation.
|
|
37
|
-
*/
|
|
38
25
|
static getSchema() {
|
|
39
26
|
return {
|
|
40
27
|
"type": "object",
|
|
41
|
-
"description": "Total count of 'buy' (long) vs 'sell' (short) positions and the sentiment ratio.",
|
|
42
28
|
"properties": {
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
"totalSellPositions": {
|
|
48
|
-
"type": "number",
|
|
49
|
-
"description": "Total count of all 'sell' (short) positions."
|
|
50
|
-
},
|
|
51
|
-
"sentimentRatio": {
|
|
52
|
-
"type": ["number", "null"],
|
|
53
|
-
"description": "Ratio of buy to sell positions (buy/sell). Null if no sell positions."
|
|
54
|
-
}
|
|
29
|
+
"long_exposure_weight": { "type": "number" },
|
|
30
|
+
"short_exposure_weight": { "type": "number" },
|
|
31
|
+
"sentiment_ratio_weight": { "type": ["number", "null"] },
|
|
32
|
+
"sentiment_ratio_count": { "type": ["number", "null"] }
|
|
55
33
|
},
|
|
56
|
-
"required": ["
|
|
34
|
+
"required": ["long_exposure_weight", "short_exposure_weight", "sentiment_ratio_weight", "sentiment_ratio_count"]
|
|
57
35
|
};
|
|
58
36
|
}
|
|
59
37
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
async process(dateStr, rootData, dependencies, config, fetchedDependencies) {
|
|
65
|
-
let totalBuyPositions = 0;
|
|
66
|
-
let totalSellPositions = 0;
|
|
67
|
-
|
|
68
|
-
// ---
|
|
69
|
-
// FIX: The test harness injects the rootData object as the *first*
|
|
70
|
-
// argument (named 'dateStr' here) for 'meta' calcs.
|
|
71
|
-
// 'rootData' (the 2nd arg) is used for yesterday's data and is null.
|
|
72
|
-
// ---
|
|
73
|
-
const rootDataToday = dateStr;
|
|
74
|
-
const insightsDoc = rootDataToday.insights;
|
|
75
|
-
|
|
76
|
-
if (!insightsDoc || !Array.isArray(insightsDoc.insights)) {
|
|
77
|
-
dependencies.logger.log('WARN', `[daily-buy-sell-sentiment-count] No 'insights' data found.`);
|
|
78
|
-
// --- STANDARD 2: SET STATE, DO NOT RETURN ---
|
|
79
|
-
this.result = {
|
|
80
|
-
totalBuyPositions: 0,
|
|
81
|
-
totalSellPositions: 0,
|
|
82
|
-
sentimentRatio: null
|
|
83
|
-
};
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
38
|
+
process(context) {
|
|
39
|
+
const { extract } = context.math;
|
|
40
|
+
const { user } = context;
|
|
41
|
+
const positions = extract.getPositions(user.portfolio.today, user.type);
|
|
86
42
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
// ---
|
|
91
|
-
// FIX: schema.md shows 'buy' and 'sell' are percentages (e.g., 51, 49).
|
|
92
|
-
// To get the *count*, we must use 'total' and the percentage.
|
|
93
|
-
// total * (buy / 100)
|
|
94
|
-
// But 'total' is the *total raw owners count*, not the total positions.
|
|
95
|
-
// The prompt says "percentage of owners in long/short", so
|
|
96
|
-
// 'buy' and 'sell' ARE the sentiment, not counts.
|
|
97
|
-
//
|
|
98
|
-
// Rereading schema.md:
|
|
99
|
-
// "buy": 51, ---> percentage of owners in long
|
|
100
|
-
// "sell": 49, ---> percentage of owners in short
|
|
101
|
-
// "total": 6149, ---> total raw owners count today
|
|
102
|
-
//
|
|
103
|
-
// The description says "Total count of 'buy' (long) vs 'sell' (short) positions".
|
|
104
|
-
// The old code `totalBuyPositions += instrument.buy;` assumes 'buy' is a count.
|
|
105
|
-
// Based on the schema, 'buy' is a *percentage*.
|
|
106
|
-
//
|
|
107
|
-
// Let's assume the intent is to calculate the weighted average sentiment.
|
|
108
|
-
// No, the schema *output* says "totalBuyPositions" and "totalSellPositions".
|
|
109
|
-
//
|
|
110
|
-
// THIS IS THE REAL BUG.
|
|
111
|
-
// The schema 'insights' does not provide *counts* of buy/sell, only *percentages*.
|
|
112
|
-
// The calculation *assumes* 'instrument.buy' is a count.
|
|
113
|
-
//
|
|
114
|
-
// Let's look at the schema again:
|
|
115
|
-
// "buy": 51
|
|
116
|
-
// "sell": 49
|
|
117
|
-
// "total": 6149
|
|
118
|
-
//
|
|
119
|
-
// The only logical interpretation is:
|
|
120
|
-
// Buy Count = total * (buy / 100)
|
|
121
|
-
// Sell Count = total * (sell / 100)
|
|
122
|
-
// ---
|
|
43
|
+
for (const pos of positions) {
|
|
44
|
+
const weight = extract.getPositionWeight(pos, user.type);
|
|
45
|
+
const direction = extract.getDirection(pos);
|
|
123
46
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
totalSellPositions += totalOwners * (instrument.sell / 100);
|
|
131
|
-
}
|
|
47
|
+
if (direction === 'Buy') {
|
|
48
|
+
this.longWeight += weight;
|
|
49
|
+
this.longCount++;
|
|
50
|
+
} else {
|
|
51
|
+
this.shortWeight += weight;
|
|
52
|
+
this.shortCount++;
|
|
132
53
|
}
|
|
133
54
|
}
|
|
134
|
-
|
|
135
|
-
// --- STANDARD 2: SET STATE, DO NOT RETURN ---
|
|
136
|
-
this.result = {
|
|
137
|
-
// Round the counts, as they are now derived from percentages
|
|
138
|
-
totalBuyPositions: Math.round(totalBuyPositions),
|
|
139
|
-
totalSellPositions: Math.round(totalSellPositions),
|
|
140
|
-
// Calculate ratio: Buy / Sell
|
|
141
|
-
sentimentRatio: (totalSellPositions > 0) ? (totalBuyPositions / totalSellPositions) : null
|
|
142
|
-
};
|
|
143
55
|
}
|
|
144
56
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
57
|
+
getResult() {
|
|
58
|
+
return {
|
|
59
|
+
long_exposure_weight: this.longWeight,
|
|
60
|
+
short_exposure_weight: this.shortWeight,
|
|
61
|
+
sentiment_ratio_weight: this.shortWeight > 0 ? this.longWeight / this.shortWeight : null,
|
|
62
|
+
sentiment_ratio_count: this.shortCount > 0 ? this.longCount / this.shortCount : null
|
|
63
|
+
};
|
|
148
64
|
}
|
|
149
65
|
|
|
150
|
-
// --- STANDARD 2: ADDED ---
|
|
151
66
|
reset() {
|
|
152
|
-
this.
|
|
67
|
+
this.longWeight = 0;
|
|
68
|
+
this.shortWeight = 0;
|
|
69
|
+
this.longCount = 0;
|
|
70
|
+
this.shortCount = 0;
|
|
153
71
|
}
|
|
154
72
|
}
|
|
155
|
-
|
|
156
|
-
module.exports = DailyBuySellSentimentCount;
|
|
73
|
+
module.exports = PlatformBuySellSentiment;
|
|
@@ -1,148 +1,92 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Calculation (Pass
|
|
3
|
-
*
|
|
4
|
-
* This metric tracks the total number of positions 'bought' (new)
|
|
5
|
-
* and 'sold' (closed) today, based on daily ownership change.
|
|
6
|
-
*
|
|
7
|
-
* This is different from 'daily_asset_activity' because it counts
|
|
8
|
-
* *positions*, not *unique users*.
|
|
2
|
+
* @fileoverview Calculation (Pass 1) for daily bought vs. sold count.
|
|
3
|
+
* REFACTORED: Uses context.math.extract on today vs yesterday.
|
|
9
4
|
*/
|
|
10
|
-
// --- STANDARD 0: REMOVED require('../../utils/sector_mapping_provider') ---
|
|
11
|
-
|
|
12
|
-
|
|
13
5
|
class DailyBoughtVsSoldCount {
|
|
14
6
|
constructor() {
|
|
15
|
-
// We will store { [instrumentId]: { new: 0, closed: 0 } }
|
|
16
7
|
this.assetActivity = new Map();
|
|
17
|
-
// --- STANDARD 0: RENAMED ---
|
|
18
8
|
this.tickerMap = null;
|
|
19
9
|
}
|
|
20
10
|
|
|
21
|
-
/**
|
|
22
|
-
* Statically defines all metadata for the manifest builder.
|
|
23
|
-
*/
|
|
24
11
|
static getMetadata() {
|
|
25
12
|
return {
|
|
26
13
|
type: 'standard',
|
|
27
14
|
rootDataDependencies: ['portfolio'],
|
|
28
|
-
isHistorical: true,
|
|
15
|
+
isHistorical: true,
|
|
29
16
|
userType: 'all',
|
|
30
17
|
category: 'core_metrics'
|
|
31
18
|
};
|
|
32
19
|
}
|
|
33
20
|
|
|
34
|
-
|
|
35
|
-
* Statically declare dependencies.
|
|
36
|
-
*/
|
|
37
|
-
static getDependencies() {
|
|
38
|
-
return [];
|
|
39
|
-
}
|
|
21
|
+
static getDependencies() { return []; }
|
|
40
22
|
|
|
41
|
-
/**
|
|
42
|
-
* Defines the output schema for this calculation.
|
|
43
|
-
*/
|
|
44
23
|
static getSchema() {
|
|
45
24
|
const tickerSchema = {
|
|
46
25
|
"type": "object",
|
|
47
|
-
"description": "Daily trade activity for a specific asset.",
|
|
48
26
|
"properties": {
|
|
49
|
-
"positions_bought": {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
},
|
|
53
|
-
"positions_sold": {
|
|
54
|
-
"type": "number",
|
|
55
|
-
"description": "Total positions closed for this asset."
|
|
56
|
-
},
|
|
57
|
-
"net_change": {
|
|
58
|
-
"type": "number",
|
|
59
|
-
"description": "Net change in positions (bought - sold)."
|
|
60
|
-
}
|
|
27
|
+
"positions_bought": { "type": "number" },
|
|
28
|
+
"positions_sold": { "type": "number" },
|
|
29
|
+
"net_change": { "type": "number" }
|
|
61
30
|
},
|
|
62
31
|
"required": ["positions_bought", "positions_sold", "net_change"]
|
|
63
32
|
};
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
"type": "object",
|
|
67
|
-
"description": "Tracks new positions (bought) and closed positions (sold) per asset.",
|
|
68
|
-
"patternProperties": {
|
|
69
|
-
"^.*$": tickerSchema // Matches any string key (ticker)
|
|
70
|
-
},
|
|
71
|
-
"additionalProperties": tickerSchema
|
|
72
|
-
};
|
|
33
|
+
return { "type": "object", "patternProperties": { "^.*$": tickerSchema } };
|
|
73
34
|
}
|
|
74
35
|
|
|
75
36
|
_initAsset(instrumentId) {
|
|
76
37
|
if (!this.assetActivity.has(instrumentId)) {
|
|
77
|
-
this.assetActivity.set(instrumentId, {
|
|
78
|
-
new: 0,
|
|
79
|
-
closed: 0
|
|
80
|
-
});
|
|
38
|
+
this.assetActivity.set(instrumentId, { new: 0, closed: 0 });
|
|
81
39
|
}
|
|
82
40
|
}
|
|
83
41
|
|
|
84
|
-
_getInstrumentIds(
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
42
|
+
_getInstrumentIds(positions, extract) {
|
|
43
|
+
const map = new Map();
|
|
44
|
+
for (const pos of positions) {
|
|
45
|
+
const posId = extract.getPositionId(pos);
|
|
46
|
+
const instId = extract.getInstrumentId(pos);
|
|
47
|
+
if (posId && instId) map.set(posId, instId);
|
|
89
48
|
}
|
|
90
|
-
|
|
91
|
-
return new Map(positions.map(p => [p.PositionID, p.InstrumentID]).filter(p => p[0] && p[1]));
|
|
49
|
+
return map;
|
|
92
50
|
}
|
|
93
51
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (!this.tickerMap)
|
|
98
|
-
this.tickerMap = context.instrumentToTicker;
|
|
99
|
-
}
|
|
52
|
+
process(context) {
|
|
53
|
+
const { extract } = context.math;
|
|
54
|
+
const { mappings, user } = context;
|
|
55
|
+
if (!this.tickerMap) this.tickerMap = mappings.instrumentToTicker;
|
|
100
56
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
57
|
+
const todayPositions = extract.getPositions(user.portfolio.today, user.type);
|
|
58
|
+
const yesterdayPositions = extract.getPositions(user.portfolio.yesterday, user.type);
|
|
104
59
|
|
|
105
|
-
const
|
|
106
|
-
const
|
|
60
|
+
const tPosMap = this._getInstrumentIds(todayPositions, extract);
|
|
61
|
+
const yPosMap = this._getInstrumentIds(yesterdayPositions, extract);
|
|
107
62
|
|
|
108
|
-
//
|
|
109
|
-
for (const [
|
|
110
|
-
if (!yPosMap.has(
|
|
111
|
-
this._initAsset(
|
|
112
|
-
this.assetActivity.get(
|
|
63
|
+
// New positions (in today, not yesterday)
|
|
64
|
+
for (const [posId, instId] of tPosMap.entries()) {
|
|
65
|
+
if (!yPosMap.has(posId)) {
|
|
66
|
+
this._initAsset(instId);
|
|
67
|
+
this.assetActivity.get(instId).new++;
|
|
113
68
|
}
|
|
114
69
|
}
|
|
115
70
|
|
|
116
|
-
//
|
|
117
|
-
for (const [
|
|
118
|
-
if (!tPosMap.has(
|
|
119
|
-
this._initAsset(
|
|
120
|
-
this.assetActivity.get(
|
|
71
|
+
// Closed positions (in yesterday, not today)
|
|
72
|
+
for (const [posId, instId] of yPosMap.entries()) {
|
|
73
|
+
if (!tPosMap.has(posId)) {
|
|
74
|
+
this._initAsset(instId);
|
|
75
|
+
this.assetActivity.get(instId).closed++;
|
|
121
76
|
}
|
|
122
77
|
}
|
|
123
78
|
}
|
|
124
79
|
|
|
125
80
|
async getResult() {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
// Failsafe check
|
|
129
|
-
if (!this.tickerMap) {
|
|
130
|
-
return {}; // process() must run first
|
|
131
|
-
}
|
|
132
|
-
|
|
81
|
+
if (!this.tickerMap) return {};
|
|
133
82
|
const result = {};
|
|
134
|
-
for (const [
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const openCount = data.new;
|
|
139
|
-
const closeCount = data.closed;
|
|
140
|
-
|
|
141
|
-
if (openCount > 0 || closeCount > 0) {
|
|
83
|
+
for (const [instId, data] of this.assetActivity.entries()) {
|
|
84
|
+
const ticker = this.tickerMap[instId] || `id_${instId}`;
|
|
85
|
+
if (data.new > 0 || data.closed > 0) {
|
|
142
86
|
result[ticker] = {
|
|
143
|
-
positions_bought:
|
|
144
|
-
positions_sold:
|
|
145
|
-
net_change:
|
|
87
|
+
positions_bought: data.new,
|
|
88
|
+
positions_sold: data.closed,
|
|
89
|
+
net_change: data.new - data.closed
|
|
146
90
|
};
|
|
147
91
|
}
|
|
148
92
|
}
|
|
@@ -151,9 +95,7 @@ class DailyBoughtVsSoldCount {
|
|
|
151
95
|
|
|
152
96
|
reset() {
|
|
153
97
|
this.assetActivity.clear();
|
|
154
|
-
// --- STANDARD 0: RENAMED ---
|
|
155
98
|
this.tickerMap = null;
|
|
156
99
|
}
|
|
157
100
|
}
|
|
158
|
-
|
|
159
101
|
module.exports = DailyBoughtVsSoldCount;
|
|
@@ -1,161 +1,103 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Calculation (Pass 1) for
|
|
3
|
-
*
|
|
4
|
-
* This metric calculates the daily change in the total number of *owners*
|
|
5
|
-
* (unique users) for each instrument.
|
|
6
|
-
*
|
|
7
|
-
* REFACTOR: This is now a 'type: "meta"' and 'isHistorical: true' calculation.
|
|
8
|
-
* It runs ONCE, loads today's and yesterday's pre-aggregated 'insights' docs,
|
|
9
|
-
* and calculates the delta based on the 'total' field.
|
|
2
|
+
* @fileoverview Calculation (Pass 1) for ownership delta (users added/lost per asset).
|
|
3
|
+
* REFACTORED: Tracks net change in number of owners.
|
|
10
4
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class DailyOwnershipDelta {
|
|
15
|
-
|
|
16
|
-
// --- STANDARD 2: ADDED ---
|
|
5
|
+
class PlatformDailyOwnershipDelta {
|
|
17
6
|
constructor() {
|
|
18
|
-
this.
|
|
7
|
+
this.assetChanges = new Map(); // { instId: { added: 0, removed: 0 } }
|
|
8
|
+
this.tickerMap = null;
|
|
19
9
|
}
|
|
20
10
|
|
|
21
|
-
/**
|
|
22
|
-
* Statically defines all metadata for the manifest builder.
|
|
23
|
-
*/
|
|
24
11
|
static getMetadata() {
|
|
25
12
|
return {
|
|
26
|
-
type: '
|
|
27
|
-
rootDataDependencies: ['
|
|
28
|
-
isHistorical: true,
|
|
29
|
-
userType: '
|
|
13
|
+
type: 'standard',
|
|
14
|
+
rootDataDependencies: ['portfolio'],
|
|
15
|
+
isHistorical: true,
|
|
16
|
+
userType: 'all',
|
|
30
17
|
category: 'core_metrics'
|
|
31
18
|
};
|
|
32
19
|
}
|
|
33
20
|
|
|
34
|
-
|
|
35
|
-
* Statically declare dependencies.
|
|
36
|
-
*/
|
|
37
|
-
static getDependencies() {
|
|
38
|
-
return [];
|
|
39
|
-
}
|
|
21
|
+
static getDependencies() { return []; }
|
|
40
22
|
|
|
41
|
-
/**
|
|
42
|
-
* Defines the output schema for this calculation.
|
|
43
|
-
*/
|
|
44
23
|
static getSchema() {
|
|
45
24
|
const tickerSchema = {
|
|
46
25
|
"type": "object",
|
|
47
|
-
"description": "Daily change in unique owners for a specific asset.",
|
|
48
26
|
"properties": {
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
},
|
|
53
|
-
"owners_yesterday": {
|
|
54
|
-
"type": "number",
|
|
55
|
-
"description": "Total unique users holding this asset yesterday."
|
|
56
|
-
},
|
|
57
|
-
"owner_delta": {
|
|
58
|
-
"type": "number",
|
|
59
|
-
"description": "The net change in unique owners (today - yesterday)."
|
|
60
|
-
},
|
|
61
|
-
"owner_delta_percent": {
|
|
62
|
-
"type": ["number", "null"],
|
|
63
|
-
"description": "Percentage change in unique owners. Null if yesterday had 0 owners."
|
|
64
|
-
}
|
|
27
|
+
"owners_added": { "type": "number" },
|
|
28
|
+
"owners_removed": { "type": "number" },
|
|
29
|
+
"net_ownership_change": { "type": "number" }
|
|
65
30
|
},
|
|
66
|
-
"required": ["
|
|
31
|
+
"required": ["owners_added", "owners_removed", "net_ownership_change"]
|
|
67
32
|
};
|
|
33
|
+
return { "type": "object", "patternProperties": { "^.*$": tickerSchema } };
|
|
34
|
+
}
|
|
68
35
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
36
|
+
_initAsset(instId) {
|
|
37
|
+
if (!this.assetChanges.has(instId)) {
|
|
38
|
+
this.assetChanges.set(instId, { added: 0, removed: 0 });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
_getOwnedInstruments(positions, extract) {
|
|
43
|
+
const set = new Set();
|
|
44
|
+
for (const pos of positions) {
|
|
45
|
+
const id = extract.getInstrumentId(pos);
|
|
46
|
+
if (id) set.add(id);
|
|
47
|
+
}
|
|
48
|
+
return set;
|
|
77
49
|
}
|
|
78
50
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// ---
|
|
99
|
-
// 2. Get Data:
|
|
100
|
-
// 'worker.js' passes the root data object as ARGUMENT 1 (named 'dateStr').
|
|
101
|
-
// ---
|
|
102
|
-
const rootDataToday = dateStr; // 'dateStr' is Param 1
|
|
103
|
-
|
|
104
|
-
const todayDoc = rootDataToday?.insights;
|
|
105
|
-
const yesterdayDoc = rootDataToday?.yesterdayInsights;
|
|
106
|
-
|
|
107
|
-
// 1. Process today's insights doc
|
|
108
|
-
if (todayDoc && Array.isArray(todayDoc.insights)) {
|
|
109
|
-
for (const instrument of todayDoc.insights) {
|
|
110
|
-
const id = instrument.instrumentId;
|
|
111
|
-
const totalOwners = instrument.total || 0; // 'total' is the owner count
|
|
112
|
-
todayOwners.set(id, totalOwners);
|
|
113
|
-
allInstrumentIds.add(id);
|
|
51
|
+
process(context) {
|
|
52
|
+
const { extract } = context.math;
|
|
53
|
+
const { mappings, user } = context;
|
|
54
|
+
if (!this.tickerMap) this.tickerMap = mappings.instrumentToTicker;
|
|
55
|
+
|
|
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
|
+
const todayPositions = extract.getPositions(user.portfolio.today, user.type);
|
|
60
|
+
const yesterdayPositions = extract.getPositions(user.portfolio.yesterday, user.type);
|
|
61
|
+
|
|
62
|
+
const tSet = this._getOwnedInstruments(todayPositions, extract);
|
|
63
|
+
const ySet = this._getOwnedInstruments(yesterdayPositions, extract);
|
|
64
|
+
|
|
65
|
+
// Added: In Today, Not Yesterday
|
|
66
|
+
for (const instId of tSet) {
|
|
67
|
+
if (!ySet.has(instId)) {
|
|
68
|
+
this._initAsset(instId);
|
|
69
|
+
this.assetChanges.get(instId).added++;
|
|
114
70
|
}
|
|
115
71
|
}
|
|
116
|
-
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
yesterdayOwners.set(id, totalOwners);
|
|
123
|
-
allInstrumentIds.add(id);
|
|
72
|
+
|
|
73
|
+
// Removed: In Yesterday, Not Today
|
|
74
|
+
for (const instId of ySet) {
|
|
75
|
+
if (!tSet.has(instId)) {
|
|
76
|
+
this._initAsset(instId);
|
|
77
|
+
this.assetChanges.get(instId).removed++;
|
|
124
78
|
}
|
|
125
79
|
}
|
|
126
|
-
|
|
127
|
-
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async getResult() {
|
|
83
|
+
if (!this.tickerMap) return {};
|
|
128
84
|
const result = {};
|
|
129
|
-
for (const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const tOwners = todayOwners.get(instrumentId) || 0;
|
|
134
|
-
const yOwners = yesterdayOwners.get(instrumentId) || 0;
|
|
135
|
-
|
|
136
|
-
if (yOwners > 0 || tOwners > 0) {
|
|
85
|
+
for (const [instId, data] of this.assetChanges.entries()) {
|
|
86
|
+
const ticker = this.tickerMap[instId] || `id_${instId}`;
|
|
87
|
+
if (data.added > 0 || data.removed > 0) {
|
|
137
88
|
result[ticker] = {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
owner_delta_percent: (yOwners > 0) ? ((tOwners - yOwners) / yOwners) * 100 : null
|
|
89
|
+
owners_added: data.added,
|
|
90
|
+
owners_removed: data.removed,
|
|
91
|
+
net_ownership_change: data.added - data.removed
|
|
142
92
|
};
|
|
143
93
|
}
|
|
144
94
|
}
|
|
145
|
-
|
|
146
|
-
// --- STANDARD 2: SET STATE, DO NOT RETURN ---
|
|
147
|
-
this.result = result;
|
|
95
|
+
return result;
|
|
148
96
|
}
|
|
149
97
|
|
|
150
|
-
// --- STANDARD 2: ADDED ---
|
|
151
|
-
async getResult(fetchedDependencies) {
|
|
152
|
-
return this.result;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// --- STANDARD 2: ADDED ---
|
|
156
98
|
reset() {
|
|
157
|
-
this.
|
|
99
|
+
this.assetChanges.clear();
|
|
100
|
+
this.tickerMap = null;
|
|
158
101
|
}
|
|
159
102
|
}
|
|
160
|
-
|
|
161
|
-
module.exports = DailyOwnershipDelta;
|
|
103
|
+
module.exports = PlatformDailyOwnershipDelta;
|