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.
@@ -7,6 +7,13 @@
7
7
  * REFACTOR: This calculation now aggregates the distribution into
8
8
  * predefined buckets on the server-side, returning a chart-ready
9
9
  * histogram object instead of raw arrays.
10
+ *
11
+ * --- FIX: 2025-11-12 ---
12
+ * This calculation is a dependency for crowd_sharpe_ratio_proxy,
13
+ * which requires sum, sumSq, and count for variance calculations.
14
+ * This file has been updated to provide *both* the histogram
15
+ * and a 'stats' object containing these required values.
16
+ * ---------------------
10
17
  */
11
18
  const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
12
19
 
@@ -31,28 +38,48 @@ class PnlDistributionPerStock {
31
38
  /**
32
39
  * Defines the output schema for this calculation.
33
40
  * REFACTOR: Schema now describes the server-calculated histogram.
34
- * @returns {object} JSON Schema object
41
+ *
42
+ * --- FIX: 2025-11-12 ---
43
+ * Added 'stats' object to the schema to support downstream
44
+ * meta-calculations like crowd_sharpe_ratio_proxy.
35
45
  */
36
46
  static getSchema() {
37
47
  const bucketSchema = {
38
48
  "type": "object",
39
- "description": "Histogram of P&L distribution for a single asset.",
49
+ "description": "Histogram and stats of P&L distribution for a single asset.",
40
50
  "properties": {
41
- "loss_heavy": { "type": "number", "description": "Count of positions with > 50% loss" },
42
- "loss_medium": { "type": "number", "description": "Count of positions with 25-50% loss" },
43
- "loss_light": { "type": "number", "description": "Count of positions with 0-25% loss" },
44
- "gain_light": { "type": "number", "description": "Count of positions with 0-25% gain" },
45
- "gain_medium": { "type": "number", "description": "Count of positions with 25-50% gain" },
46
- "gain_heavy": { "type": "number", "description": "Count of positions with 50-100% gain" },
47
- "gain_extreme": { "type": "number", "description": "Count of positions with > 100% gain" },
48
- "total_positions": { "type": "number", "description": "Total positions counted" }
51
+ "histogram": {
52
+ "type": "object",
53
+ "description": "Histogram of P&L distribution.",
54
+ "properties": {
55
+ "loss_heavy": { "type": "number", "description": "Count of positions with > 50% loss" },
56
+ "loss_medium": { "type": "number", "description": "Count of positions with 25-50% loss" },
57
+ "loss_light": { "type": "number", "description": "Count of positions with 0-25% loss" },
58
+ "gain_light": { "type": "number", "description": "Count of positions with 0-25% gain" },
59
+ "gain_medium": { "type": "number", "description": "Count of positions with 25-50% gain" },
60
+ "gain_heavy": { "type": "number", "description": "Count of positions with 50-100% gain" },
61
+ "gain_extreme": { "type": "number", "description": "Count of positions with > 100% gain" },
62
+ "total_positions": { "type": "number", "description": "Total positions counted" }
63
+ },
64
+ "required": ["total_positions"]
65
+ },
66
+ "stats": {
67
+ "type": "object",
68
+ "description": "Raw statistics needed for variance/Sharpe calculations.",
69
+ "properties": {
70
+ "sum": { "type": "number", "description": "Sum of all P&L percentages" },
71
+ "sumSq": { "type": "number", "description": "Sum of all squared P&L percentages" },
72
+ "count": { "type": "number", "description": "Total count of positions" }
73
+ },
74
+ "required": ["sum", "sumSq", "count"]
75
+ }
49
76
  },
50
- "required": ["total_positions"]
77
+ "required": ["histogram", "stats"]
51
78
  };
52
79
 
53
80
  return {
54
81
  "type": "object",
55
- "description": "Calculates a histogram of P&L percentage distribution for all open positions, per asset.",
82
+ "description": "Calculates a histogram and raw stats of P&L percentage distribution for all open positions, per asset.",
56
83
  "patternProperties": {
57
84
  "^.*$": bucketSchema // Ticker
58
85
  },
@@ -90,6 +117,9 @@ class PnlDistributionPerStock {
90
117
  /**
91
118
  * REFACTOR: This method now calculates the distribution on the server.
92
119
  * It transforms the raw P&L arrays into histogram bucket counts.
120
+ *
121
+ * --- FIX: 2025-11-12 ---
122
+ * Also calculates and returns sum, sumSq, and count.
93
123
  */
94
124
  async getResult() {
95
125
  if (!this.mappings) {
@@ -100,6 +130,11 @@ class PnlDistributionPerStock {
100
130
 
101
131
  for (const [instrumentId, pnlValues] of this.pnlMap.entries()) {
102
132
  const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
133
+ const count = pnlValues.length;
134
+
135
+ if (count === 0) {
136
+ continue;
137
+ }
103
138
 
104
139
  // 1. Initialize the histogram object for this ticker
105
140
  const histogram = {
@@ -110,11 +145,20 @@ class PnlDistributionPerStock {
110
145
  gain_medium: 0,
111
146
  gain_heavy: 0,
112
147
  gain_extreme: 0,
113
- total_positions: pnlValues.length
148
+ total_positions: count
114
149
  };
115
150
 
116
- // 2. Process all P&L values into the buckets
151
+ // --- FIX: Initialize stats ---
152
+ let sum = 0;
153
+ let sumSq = 0;
154
+
155
+ // 2. Process all P&L values into buckets and calculate stats
117
156
  for (const pnl of pnlValues) {
157
+ // --- FIX: Add to stats ---
158
+ sum += pnl;
159
+ sumSq += (pnl * pnl);
160
+
161
+ // Add to histogram
118
162
  for (const bucket of BUCKETS) {
119
163
  if (pnl >= bucket.min && pnl < bucket.max) {
120
164
  histogram[bucket.label]++;
@@ -122,9 +166,19 @@ class PnlDistributionPerStock {
122
166
  }
123
167
  }
124
168
  }
169
+
170
+ // --- FIX: Create stats object ---
171
+ const stats = {
172
+ sum: sum,
173
+ sumSq: sumSq,
174
+ count: count
175
+ };
125
176
 
126
- // 3. Add the aggregated histogram to the final result
127
- result[ticker] = histogram;
177
+ // 3. Add the aggregated histogram and stats to the final result
178
+ result[ticker] = {
179
+ histogram: histogram,
180
+ stats: stats
181
+ };
128
182
  }
129
183
  return result;
130
184
  }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * @fileoverview Calculation (Pass 1) for platform conviction divergence.
3
+ *
4
+ * This metric answers: "Is our 20,000-user sample more bullish or bearish
5
+ * on an asset than the entire eToro platform?"
6
+ *
7
+ * It compares the long/short ratio of our sample (from 'portfolio' data)
8
+ * against the platform-wide 'buy'/'sell' % (from 'insights' data).
9
+ */
10
+ const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
11
+
12
+ class PlatformConvictionDivergence {
13
+ constructor() {
14
+ // { [instrumentId]: { long: 0, short: 0 } }
15
+ this.sampledCounts = new Map();
16
+ this.mappings = null;
17
+ this.todayInsightsData = null;
18
+ }
19
+
20
+ /**
21
+ * Defines the output schema for this calculation.
22
+ * @returns {object} JSON Schema object
23
+ */
24
+ static getSchema() {
25
+ const tickerSchema = {
26
+ "type": "object",
27
+ "properties": {
28
+ "platform_long_pct": {
29
+ "type": "number",
30
+ "description": "Percentage of holders on the entire platform who are long."
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
+ }
40
+ },
41
+ "required": ["platform_long_pct", "sampled_long_pct", "divergence"]
42
+ };
43
+
44
+ return {
45
+ "type": "object",
46
+ "description": "Calculates the divergence between the sample's long/short ratio and the platform's.",
47
+ "patternProperties": {
48
+ "^.*$": tickerSchema // Ticker
49
+ },
50
+ "additionalProperties": tickerSchema
51
+ };
52
+ }
53
+
54
+ /**
55
+ * This is a Pass 1 calculation and has no dependencies.
56
+ */
57
+ static getDependencies() {
58
+ return [];
59
+ }
60
+
61
+ _initAsset(instrumentId) {
62
+ if (!this.sampledCounts.has(instrumentId)) {
63
+ this.sampledCounts.set(instrumentId, { long: 0, short: 0 });
64
+ }
65
+ }
66
+
67
+ process(portfolioData, yesterdayPortfolio, userId, context, todayInsights) {
68
+ // Capture insights data on the first user processed
69
+ if (!this.todayInsightsData && todayInsights) {
70
+ this.todayInsightsData = todayInsights;
71
+ }
72
+
73
+ const positions = portfolioData.PublicPositions || portfolioData.AggregatedPositions;
74
+ if (!positions || !Array.isArray(positions)) {
75
+ return;
76
+ }
77
+
78
+ for (const pos of positions) {
79
+ const instrumentId = pos.InstrumentID;
80
+ if (!instrumentId) continue;
81
+
82
+ this._initAsset(instrumentId);
83
+ const assetData = this.sampledCounts.get(instrumentId);
84
+
85
+ if (pos.IsBuy) {
86
+ assetData.long++;
87
+ } else {
88
+ assetData.short++;
89
+ }
90
+ }
91
+ }
92
+
93
+ async getResult() {
94
+ if (!this.mappings) {
95
+ this.mappings = await loadInstrumentMappings();
96
+ }
97
+
98
+ const result = {};
99
+ const insights = this.todayInsightsData?.insights;
100
+
101
+ if (!insights || !Array.isArray(insights)) {
102
+ return {}; // No platform data to compare against
103
+ }
104
+
105
+ for (const instrument of insights) {
106
+ const instrumentId = instrument.instrumentId;
107
+ const ticker = this.mappings.instrumentToTicker[instrumentId];
108
+ if (!ticker) continue;
109
+
110
+ const platform_long_pct = instrument.buy || 0; // e.g., 51
111
+
112
+ const sampledData = this.sampledCounts.get(instrumentId);
113
+ const sampled_long = sampledData?.long || 0;
114
+ const sampled_short = sampledData?.short || 0;
115
+ const totalSampled = sampled_long + sampled_short;
116
+
117
+ let sampled_long_pct = null;
118
+ let divergence = null;
119
+
120
+ if (totalSampled > 0) {
121
+ sampled_long_pct = (sampled_long / totalSampled) * 100;
122
+ divergence = sampled_long_pct - platform_long_pct;
123
+ }
124
+
125
+ result[ticker] = {
126
+ platform_long_pct: platform_long_pct,
127
+ sampled_long_pct: sampled_long_pct,
128
+ divergence: divergence
129
+ };
130
+ }
131
+ return result;
132
+ }
133
+
134
+ reset() {
135
+ this.sampledCounts.clear();
136
+ this.mappings = null;
137
+ this.todayInsightsData = null;
138
+ }
139
+ }
140
+
141
+ module.exports = PlatformConvictionDivergence;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiden-shared-calculations-unified",
3
- "version": "1.0.71",
3
+ "version": "1.0.73",
4
4
  "description": "Shared calculation modules for the BullTrackers Computation System.",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -24,7 +24,8 @@
24
24
  "@google-cloud/firestore": "^7.11.3",
25
25
  "sharedsetup": "latest",
26
26
  "require-all": "^3.0.0",
27
- "dotenv": "latest"
27
+ "dotenv": "latest",
28
+ "viz.js": "^2.1.2"
28
29
  },
29
30
  "devDependencies": {
30
31
  "bulltracker-deployer": "file:../bulltracker-deployer"