aiden-shared-calculations-unified 1.0.80 → 1.0.82
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 +21 -42
- package/calculations/core/price-metrics.js +372 -0
- package/calculations/core/social-sentiment-aggregation.js +12 -6
- package/calculations/helix/winner-loser-flow.js +29 -46
- package/calculations/legacy/in_loss_asset_crowd_flow.js +19 -49
- package/calculations/legacy/in_profit_asset_crowd_flow.js +20 -50
- package/node-graphviz/README.md +123 -0
- package/node-graphviz/bin/graphvizlib.wasm +0 -0
- package/node-graphviz/index.d.ts +33 -0
- package/node-graphviz/index.js +4911 -0
- package/node-graphviz/package.json +16 -0
- package/package.json +12 -4
|
@@ -7,68 +7,46 @@
|
|
|
7
7
|
* This provides a crowd-wide P&L status for each instrument.
|
|
8
8
|
*
|
|
9
9
|
* --- FIX ---
|
|
10
|
-
* This version is modified to only store
|
|
11
|
-
*
|
|
12
|
-
* 1 MiB Firestore document size limit.
|
|
10
|
+
* This version is modified to *only* store counts. The
|
|
11
|
+
* `users_in_profit` and `users_in_loss` arrays are removed
|
|
12
|
+
* to prevent exceeding the 1 MiB Firestore document size limit.
|
|
13
13
|
*/
|
|
14
14
|
const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
|
|
15
15
|
|
|
16
16
|
class AssetPnlStatus {
|
|
17
17
|
constructor() {
|
|
18
|
-
// We will store { [instrumentId]: {
|
|
19
|
-
//
|
|
18
|
+
// We will store { [instrumentId]: { in_profit_count: 0, in_loss_count: 0 } }
|
|
19
|
+
// We no longer store the user maps, just the counts.
|
|
20
20
|
this.assets = new Map();
|
|
21
21
|
this.mappings = null;
|
|
22
|
+
this.seenUsers = new Map(); // Map<instrumentId, Set<userId>>
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Defines the output schema for this calculation.
|
|
26
27
|
* --- MODIFIED ---
|
|
27
|
-
*
|
|
28
|
-
* from `items: userSchema` to `items: { "type": "string" }`.
|
|
28
|
+
* Removed the `users_in_profit` and `users_in_loss` arrays.
|
|
29
29
|
* @returns {object} JSON Schema object
|
|
30
30
|
*/
|
|
31
31
|
static getSchema() {
|
|
32
|
-
/*
|
|
33
|
-
// The userSchema is no longer needed in the output
|
|
34
|
-
const userSchema = {
|
|
35
|
-
"type": "object",
|
|
36
|
-
"properties": {
|
|
37
|
-
"userId": { "type": "string" },
|
|
38
|
-
"pnl": { "type": "number" }
|
|
39
|
-
},
|
|
40
|
-
"required": ["userId", "pnl"]
|
|
41
|
-
};
|
|
42
|
-
*/
|
|
43
|
-
|
|
44
32
|
const tickerSchema = {
|
|
45
33
|
"type": "object",
|
|
46
34
|
"description": "P&L status for a specific asset.",
|
|
47
35
|
"properties": {
|
|
48
36
|
"in_profit_count": {
|
|
49
37
|
"type": "number",
|
|
50
|
-
"description": "Count of users currently in profit on this asset."
|
|
38
|
+
"description": "Count of unique users currently in profit on this asset."
|
|
51
39
|
},
|
|
52
40
|
"in_loss_count": {
|
|
53
41
|
"type": "number",
|
|
54
|
-
"description": "Count of users currently in loss on this asset."
|
|
42
|
+
"description": "Count of unique users currently in loss on this asset."
|
|
55
43
|
},
|
|
56
44
|
"profit_ratio": {
|
|
57
45
|
"type": "number",
|
|
58
46
|
"description": "Percentage of users in profit (In Profit / Total)."
|
|
59
|
-
},
|
|
60
|
-
"users_in_profit": {
|
|
61
|
-
"type": "array",
|
|
62
|
-
"description": "List of user IDs in profit.",
|
|
63
|
-
"items": { "type": "string" } // <-- MODIFIED
|
|
64
|
-
},
|
|
65
|
-
"users_in_loss": {
|
|
66
|
-
"type": "array",
|
|
67
|
-
"description": "List of user IDs in loss.",
|
|
68
|
-
"items": { "type": "string" } // <-- MODIFIED
|
|
69
47
|
}
|
|
70
48
|
},
|
|
71
|
-
"required": ["in_profit_count", "in_loss_count", "profit_ratio"
|
|
49
|
+
"required": ["in_profit_count", "in_loss_count", "profit_ratio"]
|
|
72
50
|
};
|
|
73
51
|
|
|
74
52
|
return {
|
|
@@ -104,8 +82,8 @@ class AssetPnlStatus {
|
|
|
104
82
|
_initAsset(instrumentId) {
|
|
105
83
|
if (!this.assets.has(instrumentId)) {
|
|
106
84
|
this.assets.set(instrumentId, {
|
|
107
|
-
in_profit: new
|
|
108
|
-
in_loss: new
|
|
85
|
+
in_profit: new Set(),
|
|
86
|
+
in_loss: new Set()
|
|
109
87
|
});
|
|
110
88
|
}
|
|
111
89
|
}
|
|
@@ -124,18 +102,20 @@ class AssetPnlStatus {
|
|
|
124
102
|
const asset = this.assets.get(instrumentId);
|
|
125
103
|
const pnl = pos.NetProfit || 0;
|
|
126
104
|
|
|
105
|
+
// Only count one user once per asset
|
|
127
106
|
if (pnl > 0) {
|
|
128
|
-
asset.in_profit.
|
|
107
|
+
asset.in_profit.add(userId);
|
|
108
|
+
asset.in_loss.delete(userId); // Ensure user isn't in both
|
|
129
109
|
} else if (pnl < 0) {
|
|
130
|
-
asset.in_loss.
|
|
110
|
+
asset.in_loss.add(userId);
|
|
111
|
+
asset.in_profit.delete(userId); // Ensure user isn't in both
|
|
131
112
|
}
|
|
132
113
|
}
|
|
133
114
|
}
|
|
134
115
|
|
|
135
116
|
/**
|
|
136
117
|
* --- MODIFIED ---
|
|
137
|
-
* This now saves
|
|
138
|
-
* an array of {userId, pnl} objects to save space.
|
|
118
|
+
* This now saves only counts.
|
|
139
119
|
*/
|
|
140
120
|
async getResult() {
|
|
141
121
|
if (!this.mappings) {
|
|
@@ -154,10 +134,8 @@ class AssetPnlStatus {
|
|
|
154
134
|
result[ticker] = {
|
|
155
135
|
in_profit_count: profitCount,
|
|
156
136
|
in_loss_count: lossCount,
|
|
157
|
-
profit_ratio: (profitCount / total) * 100
|
|
158
|
-
//
|
|
159
|
-
users_in_profit: Array.from(data.in_profit.keys()), // <-- MODIFIED
|
|
160
|
-
users_in_loss: Array.from(data.in_loss.keys()) // <-- MODIFIED
|
|
137
|
+
profit_ratio: (profitCount / total) * 100
|
|
138
|
+
// Removed the user arrays
|
|
161
139
|
};
|
|
162
140
|
}
|
|
163
141
|
}
|
|
@@ -167,6 +145,7 @@ class AssetPnlStatus {
|
|
|
167
145
|
reset() {
|
|
168
146
|
this.assets.clear();
|
|
169
147
|
this.mappings = null;
|
|
148
|
+
this.seenUsers.clear();
|
|
170
149
|
}
|
|
171
150
|
}
|
|
172
151
|
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Calculation (Pass 1 - Meta) for historical price metrics.
|
|
3
|
+
*
|
|
4
|
+
* This metric answers: "What is the Volatility, Sharpe Ratio, and Max Drawdown
|
|
5
|
+
* for all instruments over 7, 30, 90, and 365-day periods?"
|
|
6
|
+
*
|
|
7
|
+
* It also aggregates these metrics as an average for each sector.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const RANGES = [7, 30, 90, 365];
|
|
11
|
+
const TRADING_DAYS_PER_YEAR = 252;
|
|
12
|
+
const MAX_LOOKBACK_DAYS = 5; // For finding a non-holiday/weekend price
|
|
13
|
+
|
|
14
|
+
class CorePriceMetrics {
|
|
15
|
+
|
|
16
|
+
// #region --- Static Metadata & Schema ---
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Statically defines all metadata for the manifest builder.
|
|
20
|
+
*/
|
|
21
|
+
static getMetadata() {
|
|
22
|
+
return {
|
|
23
|
+
type: 'meta',
|
|
24
|
+
rootDataDependencies: [], // Relies on price data, not root data
|
|
25
|
+
isHistorical: true, // Needs up to 365d of price history
|
|
26
|
+
userType: 'n/a',
|
|
27
|
+
category: 'core_metrics' // Fits with other price/metric calcs
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* This is a Pass 1 calculation and has no dependencies on other calculations.
|
|
33
|
+
*/
|
|
34
|
+
static getDependencies() {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Defines the output schema for this calculation.
|
|
40
|
+
*/
|
|
41
|
+
static getSchema() {
|
|
42
|
+
// This is the sub-schema for a single instrument's metrics
|
|
43
|
+
const instrumentMetricsSchema = {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"properties": {
|
|
46
|
+
"stdev_7d": { "type": ["number", "null"], "description": "7-day standard deviation of daily returns" },
|
|
47
|
+
"volatility_annualized_7d": { "type": ["number", "null"], "description": "7-day annualized volatility" },
|
|
48
|
+
"sharpe_ratio_7d": { "type": ["number", "null"], "description": "7-day annualized Sharpe ratio (rf=0)" },
|
|
49
|
+
"max_drawdown_7d": { "type": ["number", "null"], "description": "7-day max peak-to-trough drawdown" },
|
|
50
|
+
|
|
51
|
+
"stdev_30d": { "type": ["number", "null"], "description": "30-day standard deviation of daily returns" },
|
|
52
|
+
"volatility_annualized_30d": { "type": ["number", "null"], "description": "30-day annualized volatility" },
|
|
53
|
+
"sharpe_ratio_30d": { "type": ["number", "null"], "description": "30-day annualized Sharpe ratio (rf=0)" },
|
|
54
|
+
"max_drawdown_30d": { "type": ["number", "null"], "description": "30-day max peak-to-trough drawdown" },
|
|
55
|
+
|
|
56
|
+
"stdev_90d": { "type": ["number", "null"], "description": "90-day standard deviation of daily returns" },
|
|
57
|
+
"volatility_annualized_90d": { "type": ["number", "null"], "description": "90-day annualized volatility" },
|
|
58
|
+
"sharpe_ratio_90d": { "type": ["number", "null"], "description": "90-day annualized Sharpe ratio (rf=0)" },
|
|
59
|
+
"max_drawdown_90d": { "type": ["number", "null"], "description": "90-day max peak-to-trough drawdown" },
|
|
60
|
+
|
|
61
|
+
"stdev_365d": { "type": ["number", "null"], "description": "365-day standard deviation of daily returns" },
|
|
62
|
+
"volatility_annualized_365d": { "type": ["number", "null"], "description": "365-day annualized volatility" },
|
|
63
|
+
"sharpe_ratio_365d": { "type": ["number", "null"], "description": "365-day annualized Sharpe ratio (rf=0)" },
|
|
64
|
+
"max_drawdown_365d": { "type": ["number", "null"], "description": "365-day max peak-to-trough drawdown" }
|
|
65
|
+
},
|
|
66
|
+
"additionalProperties": false
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// This is the sub-schema for a single sector's *averages*
|
|
70
|
+
const sectorMetricsSchema = {
|
|
71
|
+
"type": "object",
|
|
72
|
+
"properties": {
|
|
73
|
+
"average_stdev_7d": { "type": ["number", "null"], "description": "Average 7-day standard deviation for the sector" },
|
|
74
|
+
"average_volatility_annualized_7d": { "type": ["number", "null"], "description": "Average 7-day annualized volatility for the sector" },
|
|
75
|
+
"average_sharpe_ratio_7d": { "type": ["number", "null"], "description": "Average 7-day annualized Sharpe ratio for the sector" },
|
|
76
|
+
"average_max_drawdown_7d": { "type": ["number", "null"], "description": "Average 7-day max drawdown for the sector" },
|
|
77
|
+
|
|
78
|
+
"average_stdev_30d": { "type": ["number", "null"], "description": "Average 30-day standard deviation for the sector" },
|
|
79
|
+
"average_volatility_annualized_30d": { "type": ["number", "null"], "description": "Average 30-day annualized volatility for the sector" },
|
|
80
|
+
"average_sharpe_ratio_30d": { "type": ["number", "null"], "description": "Average 30-day annualized Sharpe ratio for the sector" },
|
|
81
|
+
"average_max_drawdown_30d": { "type": ["number", "null"], "description": "Average 30-day max drawdown for the sector" },
|
|
82
|
+
|
|
83
|
+
"average_stdev_90d": { "type": ["number", "null"], "description": "Average 90-day standard deviation for the sector" },
|
|
84
|
+
"average_volatility_annualized_90d": { "type": ["number", "null"], "description": "Average 90-day annualized volatility for the sector" },
|
|
85
|
+
"average_sharpe_ratio_90d": { "type": ["number", "null"], "description": "Average 90-day annualized Sharpe ratio for the sector" },
|
|
86
|
+
"average_max_drawdown_90d": { "type": ["number", "null"], "description": "Average 90-day max drawdown for the sector" },
|
|
87
|
+
|
|
88
|
+
"average_stdev_365d": { "type": ["number", "null"], "description": "Average 365-day standard deviation for the sector" },
|
|
89
|
+
"average_volatility_annualized_365d": { "type": ["number", "null"], "description": "Average 365-day annualized volatility for the sector" },
|
|
90
|
+
"average_sharpe_ratio_365d": { "type": ["number", "null"], "description": "Average 365-day annualized Sharpe ratio for the sector" },
|
|
91
|
+
"average_max_drawdown_365d": { "type": ["number", "null"], "description": "Average 365-day max drawdown for the sector" }
|
|
92
|
+
},
|
|
93
|
+
"additionalProperties": false
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// This is the final, top-level schema
|
|
97
|
+
return {
|
|
98
|
+
"type": "object",
|
|
99
|
+
"description": "Calculates risk/return metrics (StdDev, Sharpe, Vol, Drawdown) for instruments and sectors.",
|
|
100
|
+
"properties": {
|
|
101
|
+
"by_instrument": {
|
|
102
|
+
"type": "object",
|
|
103
|
+
"description": "Metrics per instrument, keyed by Ticker.",
|
|
104
|
+
"patternProperties": { "^[A-Z\\.]+$": instrumentMetricsSchema }, // Match tickers like 'NVDA' or 'BRK.B'
|
|
105
|
+
"additionalProperties": instrumentMetricsSchema
|
|
106
|
+
},
|
|
107
|
+
"by_sector": {
|
|
108
|
+
"type": "object",
|
|
109
|
+
"description": "Average metrics per sector, keyed by Sector Name.",
|
|
110
|
+
"patternProperties": { "^[a-zA-Z0-9_ ]+$": sectorMetricsSchema }, // Match sector names
|
|
111
|
+
"additionalProperties": sectorMetricsSchema
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
"required": ["by_instrument", "by_sector"]
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// #endregion --- Static Metadata & Schema ---
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
// #region --- Main Process ---
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* This is a 'meta' calculation. It runs once.
|
|
125
|
+
* @param {string} dateStr - The date string 'YYYY-MM-DD'.
|
|
126
|
+
* @param {object} dependencies - The shared dependencies (e.g., logger, calculationUtils).
|
|
127
|
+
* @param {object} config - The computation system configuration.
|
|
128
|
+
* @returns {Promise<object>} The calculation result.
|
|
129
|
+
*/
|
|
130
|
+
async process(dateStr, dependencies, config) {
|
|
131
|
+
const { logger, calculationUtils } = dependencies;
|
|
132
|
+
|
|
133
|
+
const priceMap = await calculationUtils.loadAllPriceData();
|
|
134
|
+
const mappings = await calculationUtils.loadInstrumentMappings();
|
|
135
|
+
|
|
136
|
+
if (!priceMap || !mappings || !mappings.instrumentToTicker || !mappings.instrumentToSector) {
|
|
137
|
+
logger.log('ERROR', '[core-price-metrics] Failed to load priceMap or mappings.');
|
|
138
|
+
return { by_instrument: {}, by_sector: {} };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const { instrumentToTicker, instrumentToSector } = mappings;
|
|
142
|
+
const by_instrument = {};
|
|
143
|
+
|
|
144
|
+
// 1. Calculate Per-Instrument Metrics
|
|
145
|
+
for (const instrumentId in priceMap) {
|
|
146
|
+
const ticker = instrumentToTicker[instrumentId];
|
|
147
|
+
if (!ticker) continue;
|
|
148
|
+
|
|
149
|
+
const priceHistoryObj = priceMap[instrumentId];
|
|
150
|
+
const instrumentMetrics = {};
|
|
151
|
+
|
|
152
|
+
// Null-out all metrics by default to ensure consistent schema
|
|
153
|
+
for (const range of RANGES) {
|
|
154
|
+
instrumentMetrics[`stdev_${range}d`] = null;
|
|
155
|
+
instrumentMetrics[`volatility_annualized_${range}d`] = null;
|
|
156
|
+
instrumentMetrics[`sharpe_ratio_${range}d`] = null;
|
|
157
|
+
instrumentMetrics[`max_drawdown_${range}d`] = null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const range of RANGES) {
|
|
161
|
+
// Get the price slice for the range (e.g., last 30 days)
|
|
162
|
+
// We need range + 1 prices to calculate `range` number of returns
|
|
163
|
+
const priceArray = this._getHistoricalPriceArray(priceHistoryObj, dateStr, range + 1);
|
|
164
|
+
|
|
165
|
+
if (priceArray.length < 2) {
|
|
166
|
+
continue; // Not enough data for this range
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Calculate drawdown on prices
|
|
170
|
+
instrumentMetrics[`max_drawdown_${range}d`] = this._calculateMaxDrawdown(priceArray);
|
|
171
|
+
|
|
172
|
+
// Calculate returns and return-based metrics
|
|
173
|
+
const dailyReturns = this._calculateDailyReturns(priceArray);
|
|
174
|
+
if (dailyReturns.length < 2) {
|
|
175
|
+
continue; // Not enough returns to calculate stddev
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const meanReturn = this._calculateMean(dailyReturns);
|
|
179
|
+
const stdDev = this._calculateStdDev(dailyReturns, meanReturn);
|
|
180
|
+
|
|
181
|
+
instrumentMetrics[`stdev_${range}d`] = stdDev;
|
|
182
|
+
|
|
183
|
+
if (stdDev > 0) {
|
|
184
|
+
instrumentMetrics[`sharpe_ratio_${range}d`] = (meanReturn / stdDev) * Math.sqrt(TRADING_DAYS_PER_YEAR);
|
|
185
|
+
instrumentMetrics[`volatility_annualized_${range}d`] = stdDev * Math.sqrt(TRADING_DAYS_PER_YEAR);
|
|
186
|
+
} else {
|
|
187
|
+
instrumentMetrics[`sharpe_ratio_${range}d`] = 0;
|
|
188
|
+
instrumentMetrics[`volatility_annualized_${range}d`] = 0;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
by_instrument[ticker] = instrumentMetrics;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 2. Calculate Sector Aggregates
|
|
196
|
+
const by_sector = this._aggregateMetricsBySector(by_instrument, instrumentToTicker, instrumentToSector);
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
by_instrument,
|
|
200
|
+
by_sector,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// #endregion --- Main Process ---
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
// #region --- Aggregation Helpers ---
|
|
208
|
+
|
|
209
|
+
_aggregateMetricsBySector(by_instrument, instrumentToTicker, instrumentToSector) {
|
|
210
|
+
const sectorAggregates = {}; // { [sector]: { metrics: { [metricName]: sum }, counts: { [metricName]: count } } }
|
|
211
|
+
const tickerToInstrument = Object.fromEntries(Object.entries(instrumentToTicker).map(([id, ticker]) => [ticker, id]));
|
|
212
|
+
|
|
213
|
+
for (const ticker in by_instrument) {
|
|
214
|
+
const instrumentId = tickerToInstrument[ticker];
|
|
215
|
+
const sector = instrumentToSector[instrumentId] || "Unknown";
|
|
216
|
+
const metrics = by_instrument[ticker];
|
|
217
|
+
|
|
218
|
+
if (!sectorAggregates[sector]) {
|
|
219
|
+
sectorAggregates[sector] = { metrics: {}, counts: {} };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
for (const metricName in metrics) {
|
|
223
|
+
const value = metrics[metricName];
|
|
224
|
+
// Check for valid, non-null, finite numbers
|
|
225
|
+
if (value !== null && typeof value === 'number' && isFinite(value)) {
|
|
226
|
+
if (!sectorAggregates[sector].metrics[metricName]) {
|
|
227
|
+
sectorAggregates[sector].metrics[metricName] = 0;
|
|
228
|
+
sectorAggregates[sector].counts[metricName] = 0;
|
|
229
|
+
}
|
|
230
|
+
sectorAggregates[sector].metrics[metricName] += value;
|
|
231
|
+
sectorAggregates[sector].counts[metricName]++;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Finalize averages
|
|
237
|
+
const by_sector = {};
|
|
238
|
+
for (const sector in sectorAggregates) {
|
|
239
|
+
by_sector[sector] = {};
|
|
240
|
+
const agg = sectorAggregates[sector];
|
|
241
|
+
|
|
242
|
+
// Get all unique metric names from this sector's aggregation
|
|
243
|
+
const allMetricNames = Object.keys(agg.metrics);
|
|
244
|
+
|
|
245
|
+
for (const metricName of allMetricNames) {
|
|
246
|
+
const count = agg.counts[metricName];
|
|
247
|
+
if (count > 0) {
|
|
248
|
+
by_sector[sector][`average_${metricName}`] = agg.metrics[metricName] / count;
|
|
249
|
+
} else {
|
|
250
|
+
by_sector[sector][`average_${metricName}`] = null; // Ensure null if no valid data
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return by_sector;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// #endregion --- Aggregation Helpers ---
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
// #region --- Math & Price Helpers ---
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Re-implementation of the logic from price_data_provider.js's private helper.
|
|
264
|
+
* Finds the most recent available price on or before a given date.
|
|
265
|
+
*/
|
|
266
|
+
_findPriceOnOrBefore(priceHistory, dateStr) {
|
|
267
|
+
if (!priceHistory) return null;
|
|
268
|
+
|
|
269
|
+
let checkDate = new Date(dateStr + 'T00:00:00Z');
|
|
270
|
+
|
|
271
|
+
for (let i = 0; i < MAX_LOOKBACK_DAYS; i++) {
|
|
272
|
+
const checkDateStr = checkDate.toISOString().slice(0, 10);
|
|
273
|
+
const price = priceHistory[checkDateStr];
|
|
274
|
+
|
|
275
|
+
if (price !== undefined && price !== null && price > 0) {
|
|
276
|
+
return price; // Found it
|
|
277
|
+
}
|
|
278
|
+
// If not found, look back one more day
|
|
279
|
+
checkDate.setUTCDate(checkDate.getUTCDate() - 1);
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Gets a gap-filled array of prices for a historical range.
|
|
286
|
+
* @param {object} priceHistoryObj - The map of { "YYYY-MM-DD": price }
|
|
287
|
+
* @param {string} endDateStr - The end date of the period (e.g., today).
|
|
288
|
+
* @param {number} numDays - The number of calendar days to fetch prices for.
|
|
289
|
+
* @returns {number[]} A sorted array of prices, oldest to newest.
|
|
290
|
+
*/
|
|
291
|
+
_getHistoricalPriceArray(priceHistoryObj, endDateStr, numDays) {
|
|
292
|
+
const prices = [];
|
|
293
|
+
let currentDate = new Date(endDateStr + 'T00:00:00Z');
|
|
294
|
+
let lastPrice = null;
|
|
295
|
+
|
|
296
|
+
for (let i = 0; i < numDays; i++) {
|
|
297
|
+
const targetDateStr = currentDate.toISOString().slice(0, 10);
|
|
298
|
+
let price = this._findPriceOnOrBefore(priceHistoryObj, targetDateStr);
|
|
299
|
+
|
|
300
|
+
// If price is null (e.g., new instrument), try to use the last known price
|
|
301
|
+
if (price === null) {
|
|
302
|
+
price = lastPrice;
|
|
303
|
+
} else {
|
|
304
|
+
lastPrice = price; // Update last known price
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (price !== null) {
|
|
308
|
+
prices.push(price);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Go back one calendar day for the next data point
|
|
312
|
+
currentDate.setUTCDate(currentDate.getUTCDate() - 1);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// We built the array from newest to oldest, so reverse it.
|
|
316
|
+
// And filter out any initial nulls if lastPrice was null at the start
|
|
317
|
+
return prices.reverse().filter(p => p !== null);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
_calculateMean(arr) {
|
|
322
|
+
if (!arr || arr.length === 0) return 0;
|
|
323
|
+
const sum = arr.reduce((acc, val) => acc + val, 0);
|
|
324
|
+
return sum / arr.length;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
_calculateStdDev(arr, mean) {
|
|
328
|
+
if (!arr || arr.length < 2) return 0;
|
|
329
|
+
const avg = mean === undefined ? this._calculateMean(arr) : mean;
|
|
330
|
+
// Use N-1 for sample standard deviation
|
|
331
|
+
const variance = arr.reduce((acc, val) => acc + (val - avg) ** 2, 0) / (arr.length - 1);
|
|
332
|
+
return Math.sqrt(variance);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
_calculateDailyReturns(prices) {
|
|
336
|
+
const returns = [];
|
|
337
|
+
for (let i = 1; i < prices.length; i++) {
|
|
338
|
+
const prevPrice = prices[i - 1];
|
|
339
|
+
const currPrice = prices[i];
|
|
340
|
+
if (prevPrice !== 0 && prevPrice !== null && currPrice !== null) {
|
|
341
|
+
returns.push((currPrice - prevPrice) / prevPrice);
|
|
342
|
+
} else {
|
|
343
|
+
returns.push(0);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return returns;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
_calculateMaxDrawdown(prices) {
|
|
350
|
+
if (!prices || prices.length < 2) return 0;
|
|
351
|
+
let maxDrawdown = 0;
|
|
352
|
+
let peak = -Infinity;
|
|
353
|
+
|
|
354
|
+
for (const price of prices) {
|
|
355
|
+
if (price > peak) {
|
|
356
|
+
peak = price;
|
|
357
|
+
}
|
|
358
|
+
if (peak > 0) { // Only calculate drawdown if peak is positive
|
|
359
|
+
const drawdown = (price - peak) / peak;
|
|
360
|
+
if (drawdown < maxDrawdown) {
|
|
361
|
+
maxDrawdown = drawdown;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// Ensure result is a finite number, default to 0
|
|
366
|
+
return isFinite(maxDrawdown) ? maxDrawdown : 0;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// #endregion --- Math & Price Helpers ---
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
module.exports = CorePriceMetrics;
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* This metric answers: "What is the aggregated social media
|
|
5
5
|
* sentiment (bullish, bearish, neutral) globally and
|
|
6
6
|
* for each ticker?"
|
|
7
|
+
*
|
|
8
|
+
* --- MODIFIED ---
|
|
9
|
+
* Changed `process` method to read from `dependencies.rootData.todaySocialPostInsights`
|
|
10
|
+
* instead of a non-existent `calculationUtils.loadSocialData`.
|
|
7
11
|
*/
|
|
8
12
|
class SocialSentimentAggregation {
|
|
9
13
|
constructor() {
|
|
@@ -90,19 +94,21 @@ class SocialSentimentAggregation {
|
|
|
90
94
|
|
|
91
95
|
/**
|
|
92
96
|
* @param {string} dateStr - Today's date.
|
|
93
|
-
* @param {object} dependencies - db, logger.
|
|
97
|
+
* @param {object} dependencies - db, logger, calculationUtils, AND rootData.
|
|
94
98
|
* @param {object} config - Computation config.
|
|
95
99
|
* @param {object} fetchedDependencies - (UNUSED) In-memory results.
|
|
96
100
|
*/
|
|
97
101
|
async process(dateStr, dependencies, config, fetchedDependencies) {
|
|
98
|
-
|
|
99
|
-
//
|
|
100
|
-
const todaySocialPosts =
|
|
102
|
+
// --- MODIFIED ---
|
|
103
|
+
// 'meta' calcs get rootData injected into the dependencies object.
|
|
104
|
+
const todaySocialPosts = dependencies.rootData?.todaySocialPostInsights || {};
|
|
105
|
+
// --- END MODIFIED ---
|
|
101
106
|
|
|
102
|
-
for (const post of todaySocialPosts) {
|
|
107
|
+
for (const post of Object.values(todaySocialPosts)) {
|
|
103
108
|
// This logic assumes 'sentiment' is a simple string,
|
|
104
109
|
// not the object from gemini. This is correct for this calc.
|
|
105
|
-
|
|
110
|
+
// --- MODIFIED: Read from structured sentiment object ---
|
|
111
|
+
const sentiment = post.sentiment?.overallSentiment?.toLowerCase() || 'neutral';
|
|
106
112
|
|
|
107
113
|
// 1. Aggregate global sentiment
|
|
108
114
|
if (sentiment === 'bullish') {
|