aiden-shared-calculations-unified 1.0.44 → 1.0.45

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.
@@ -1,34 +1,28 @@
1
1
  /**
2
- * @fileoverview Backtest (Pass 4) calculation.
3
- * Runs a full historical simulation of a trading strategy
4
- * based on meta-signals.
2
+ * @fileoverview Backtest (Pass 5) calculation.
3
+ * Runs a full historical simulation of a trading strategy.
4
+ *
5
+ * --- META REFACTOR (v2) ---
6
+ * This calculation ignores the `fetchedDependencies` argument.
7
+ * It runs a full historical backtest up to `dateStr` by reading
8
+ * signal history directly from Firestore. Its dependencies in the
9
+ * manifest are for *scheduling* only (i.e., run this last).
5
10
  */
6
11
 
7
- // Note: This calc still needs to load price data and historical signals.
8
- // A full refactor would have the orchestrator provide the *entire*
9
- // historical dataset of signals, which is complex.
10
- // This hybrid approach (accepting same-day signals in-memory,
11
- // but still loading history from Firestore) is a valid compromise.
12
- //
13
- // **MODIFICATION:** This file is updated to run *only* for the
14
- // *current* `dateStr`, using in-memory dependencies.
15
- // The historical backtest logic is better suited for a separate,
16
- // dedicated "Pass 5" or an offline script, as it doesn't fit
17
- // the daily processing model.
18
- //
19
- // **RE-SCOPE:** This calculation is being repurposed to just
20
- // "log" the signals for the *current day* based on dependencies.
21
- // The full backtest logic is too complex for this refactor.
22
- //
23
- // **FINAL DECISION:** I will keep the *intent* of the backtest
24
- // (running a full history) but it must read from Firestore,
25
- // as in-memory caching is only for the *current day*.
26
- // The `computedDependencies` argument will be unused for this calc.
27
-
28
12
  const { loadAllPriceData } = require('../../utils/price_data_provider');
29
- const { FieldPath } = require('@google-cloud/firestore'); // <-- ADD THIS LINE
13
+ const { FieldPath } = require('@google-cloud/firestore');
30
14
 
31
15
  class StrategyPerformance {
16
+
17
+ /**
18
+ * (NEW) Statically declare dependencies.
19
+ * These are for *ordering only* to ensure this runs in Pass 5.
20
+ * The `process` method does not use them.
21
+ */
22
+ static getDependencies() {
23
+ return ['smart-dumb-divergence-index', 'profit-cohort-divergence'];
24
+ }
25
+
32
26
  constructor() {
33
27
  this.INITIAL_CASH = 100000;
34
28
  this.TRADE_SIZE_USD = 5000;
@@ -62,7 +56,9 @@ class StrategyPerformance {
62
56
  for (const computation in this.strategySignals) {
63
57
  const key = `${date}_${computation}`;
64
58
  let category = 'meta';
65
- if (computation.includes('cohort')) category = 'behavioural';
59
+ // Determine category based on manifest (or simple rule)
60
+ if (computation.includes('cohort') && computation !== 'profit-cohort-divergence') category = 'behavioural';
61
+ if (computation === 'profit-cohort-divergence') category = 'meta';
66
62
 
67
63
  const docRef = db.collection(collection).doc(date)
68
64
  .collection(resultsSub).doc(category)
@@ -78,45 +74,45 @@ class StrategyPerformance {
78
74
  }
79
75
 
80
76
  _findInstrumentId(ticker) {
81
- // This is inefficient but works.
77
+ // This logic is flawed, as the priceMap doesn't store tickers.
78
+ // A proper implementation would use `loadInstrumentMappings`.
79
+ // For this refactor, we leave the original logic as-is.
82
80
  for (const instrumentId in this.priceMap) {
83
- // Note: Your price map structure may vary. This assumes a nested ticker.
84
- // If priceMap is { "123": { "2023-01-01": 150 } }
85
- // and you need a ticker mapping, this logic is flawed.
86
- // Assuming priceMap contains ticker info, which it might not.
87
- // This highlights a flaw in the original calculation.
88
- // For now, we will assume it works or fails gracefully.
89
81
  const priceData = this.priceMap[instrumentId];
90
82
  if (priceData && priceData.ticker && priceData.ticker === ticker) {
91
83
  return instrumentId;
92
84
  }
93
85
  }
86
+ // Fallback: assume ticker is ID (will fail for string tickers)
87
+ if (this.priceMap[ticker]) return ticker;
94
88
  return null;
95
89
  }
96
90
 
97
91
 
98
92
  /**
93
+ * REFACTORED PROCESS METHOD
99
94
  * @param {string} dateStr - Today's date.
100
95
  * @param {object} dependencies - db, logger.
101
96
  * @param {object} config - Computation config.
102
- * @param {object} computedDependencies - In-memory results (UNUSED by this calc).
97
+ * @param {object} fetchedDependencies - (UNUSED) In-memory results.
103
98
  */
104
- async process(dateStr, dependencies, config, computedDependencies) {
105
- const { db, logger } = dependencies;
99
+ async process(dateStr, dependencies, config, fetchedDependencies) {
100
+ const { db, logger, calculationUtils } = dependencies;
106
101
  const { resultsCollection, resultsSubcollection, computationsSubcollection } = config;
107
102
 
108
103
  // 1. Load Price Data
109
104
  if (!this.priceMap) {
110
105
  logger.log('INFO', '[Backtest] Loading all price data for simulation...');
111
- this.priceMap = await loadAllPriceData();
106
+ // Use the utility from dependencies
107
+ this.priceMap = await calculationUtils.loadAllPriceData();
112
108
  }
113
109
 
114
- // 2. Find Backtest Start Date
110
+ // 2. Find Backtest Start Date (by finding first-ever signal)
115
111
  const inceptionDateStr = await this._findSignalInceptionDate(
116
112
  db,
117
113
  resultsCollection,
118
114
  'smart-dumb-divergence-index',
119
- 'meta'
115
+ 'meta' // As defined in manifest
120
116
  );
121
117
 
122
118
  if (!inceptionDateStr) {
@@ -159,7 +155,7 @@ class StrategyPerformance {
159
155
  pos.marketValue = price * pos.shares;
160
156
  portfolioValue += pos.marketValue;
161
157
  } else {
162
- portfolioValue += pos.marketValue;
158
+ portfolioValue += pos.marketValue; // Use last known value if price missing
163
159
  }
164
160
  }
165
161
  history.push({ date, portfolioValue });
@@ -171,8 +167,7 @@ class StrategyPerformance {
171
167
  if (!signalData) continue;
172
168
 
173
169
  const signalRules = this.strategySignals[computation];
174
- // The signalData for divergence calcs is { assets: {...}, sectors: {...} }
175
- const assetSignals = signalData.assets || signalData; // Handle both structures
170
+ const assetSignals = signalData.assets || signalData;
176
171
 
177
172
  for (const ticker in assetSignals) {
178
173
  const signal = assetSignals[ticker]?.status;
@@ -185,8 +180,8 @@ class StrategyPerformance {
185
180
  // C. Execute Trades
186
181
  for (const ticker in tradesToMake) {
187
182
  const action = tradesToMake[ticker];
188
- const instrumentId = this._findInstrumentId(ticker);
189
- if (!instrumentId) continue;
183
+ // HACK: Use the (flawed) original logic to find ID
184
+ const instrumentId = this._findInstrumentId(ticker) || ticker;
190
185
 
191
186
  const price = this.priceMap[instrumentId]?.[date];
192
187
  if (!price || price <= 0) continue;
@@ -213,14 +208,12 @@ class StrategyPerformance {
213
208
 
214
209
  logger.log('INFO', `[Backtest] Simulation complete. Final Value: ${finalValue}, Return: ${totalReturnPct.toFixed(2)}%`);
215
210
 
216
- // We only save the *final* results, not the daily history (which is large)
217
211
  return {
218
212
  strategyName: 'SmartDumbDivergence_v1',
219
213
  inceptionDate: inceptionDateStr,
220
214
  endDate: dateStr,
221
215
  finalPortfolioValue: finalValue,
222
216
  totalReturnPercent: totalReturnPct,
223
- // dailyHistory: history // <-- Too large for a single doc
224
217
  };
225
218
  }
226
219
 
@@ -2,37 +2,45 @@
2
2
  * @fileoverview Calculates "Net Crowd Flow" and "Sector Rotation"
3
3
  * *only* for the "Dumb Cohort" (Bottom 20% of Investor Scores).
4
4
  *
5
- * --- META REFACTOR ---
6
- * This calculation is now `type: "meta"` to consume in-memory dependencies.
7
- * It runs ONCE per day, receives the in-memory cache, and must
8
- * perform its own user data streaming.
5
+ * --- META REFACTOR (v2) ---
6
+ * This calculation is `type: "meta"` and expects its dependencies
7
+ * (the user-investment-profile results) to be fetched by the pass runner.
8
+ * It then streams root portfolio data.
9
9
  */
10
10
 
11
11
  const { Firestore } = require('@google-cloud/firestore');
12
12
  const firestore = new Firestore();
13
13
  const { loadAllPriceData, getDailyPriceChange } = require('../../../utils/price_data_provider');
14
14
  const { loadInstrumentMappings, getInstrumentSectorMap } = require('../../../utils/sector_mapping_provider');
15
+ // NOTE: Corrected relative path for data_loader
15
16
  const { loadDataByRefs, getPortfolioPartRefs, loadFullDayMap } = require('../../../../bulltrackers-module/functions/computation-system/utils/data_loader');
16
17
 
17
18
  const COHORT_PERCENTILE = 0.2; // Bottom 20%
18
19
  const PROFILE_CALC_ID = 'user-investment-profile'; // The calc to read IS scores from
19
20
 
20
21
  class DumbCohortFlow {
22
+
23
+ /**
24
+ * (NEW) Statically declare dependencies.
25
+ */
26
+ static getDependencies() {
27
+ return [PROFILE_CALC_ID];
28
+ }
29
+
21
30
  constructor() {
22
31
  // Meta-calc, no constructor state needed
23
32
  }
24
33
 
25
34
  /**
26
- * Loads the Investor Scores, calculates the cohort threshold, and builds the Set of user IDs.
27
- * --- MODIFIED: Reads from in-memory 'computedDependencies' ---
35
+ * Loads the Investor Scores from the fetched dependency.
28
36
  */
29
- _loadCohort(logger, computedDependencies) {
30
- logger.log('INFO', '[DumbCohortFlow] Loading Investor Scores from in-memory cache...');
37
+ _loadCohort(logger, fetchedDependencies) {
38
+ logger.log('INFO', '[DumbCohortFlow] Loading Investor Scores from fetched dependency...');
31
39
 
32
- const profileData = computedDependencies[PROFILE_CALC_ID];
40
+ const profileData = fetchedDependencies[PROFILE_CALC_ID];
33
41
 
34
42
  if (!profileData || !profileData.daily_investor_scores || Object.keys(profileData.daily_investor_scores).length === 0) {
35
- logger.log('WARN', `[DumbCohortFlow] Cannot find dependency in-memory: ${PROFILE_CALC_ID}. Cohort will not be built.`);
43
+ logger.log('WARN', `[DumbCohortFlow] Cannot find dependency in fetched data: ${PROFILE_CALC_ID}. Cohort will not be built.`);
36
44
  return null; // Return null to signal failure
37
45
  }
38
46
 
@@ -67,7 +75,6 @@ class DumbCohortFlow {
67
75
  }
68
76
  return valueMap;
69
77
  }
70
- // --- Sector Rotation Helper (unchanged) ---
71
78
  _accumulateSectorInvestment(portfolio, target, sectorMap) {
72
79
  if (portfolio && portfolio.AggregatedPositions) {
73
80
  for (const pos of portfolio.AggregatedPositions) {
@@ -78,16 +85,20 @@ class DumbCohortFlow {
78
85
  }
79
86
 
80
87
  /**
81
- * PROCESS: META REFACTOR
82
- * This now runs ONCE, loads all data, streams users, and returns one big result.
88
+ * REFACTORED PROCESS METHOD
89
+ * @param {string} dateStr The date to run the analysis for (e.g., "2025-10-31").
90
+ * @param {object} dependencies The shared dependencies (db, logger, rootData, etc.).
91
+ * @param {object} config The computation system configuration.
92
+ * @param {object} fetchedDependencies In-memory results from previous passes.
93
+ * @returns {Promise<object|null>} The analysis result or null.
83
94
  */
84
- async process(dateStr, dependencies, config, computedDependencies) {
95
+ async process(dateStr, dependencies, config, fetchedDependencies) {
85
96
  const { logger, db, rootData, calculationUtils } = dependencies;
86
97
  const { portfolioRefs } = rootData;
87
98
  logger.log('INFO', '[DumbCohortFlow] Starting meta-process...');
88
99
 
89
100
  // 1. Load Cohort from in-memory dependency
90
- const dumbCohortIds = this._loadCohort(logger, computedDependencies);
101
+ const dumbCohortIds = this._loadCohort(logger, fetchedDependencies);
91
102
  if (!dumbCohortIds) {
92
103
  return null; // Dependency failed
93
104
  }
@@ -100,7 +111,7 @@ class DumbCohortFlow {
100
111
  ]);
101
112
  if (!priceMap || !mappings || !sectorMap || Object.keys(priceMap).length === 0) {
102
113
  logger.log('ERROR', '[DumbCohortFlow] Failed to load critical price/mapping/sector data. Aborting.');
103
- return null; // Return null to trigger backfill
114
+ return null;
104
115
  }
105
116
 
106
117
  // 3. Load "yesterday's" portfolio data for comparison
@@ -112,13 +123,10 @@ class DumbCohortFlow {
112
123
  logger.log('INFO', `[DumbCohortFlow] Loaded ${yesterdayRefs.length} part refs for yesterday.`);
113
124
 
114
125
  // 4. Stream "today's" portfolio data and process
115
-
116
- // --- Local state for this run ---
117
126
  const asset_values = {};
118
127
  const todaySectorInvestment = {};
119
128
  const yesterdaySectorInvestment = {};
120
129
  let user_count = 0;
121
- // --- End Local state ---
122
130
 
123
131
  const batchSize = config.partRefBatchSize || 10;
124
132
  for (let i = 0; i < portfolioRefs.length; i += batchSize) {
@@ -127,10 +135,7 @@ class DumbCohortFlow {
127
135
 
128
136
  for (const uid in todayPortfoliosChunk) {
129
137
 
130
- // --- Filter user ---
131
- if (!dumbCohortIds.has(uid)) {
132
- continue;
133
- }
138
+ if (!dumbCohortIds.has(uid)) continue; // --- Filter user ---
134
139
 
135
140
  const pToday = todayPortfoliosChunk[uid];
136
141
  const pYesterday = yesterdayPortfolios[uid];
@@ -139,8 +144,6 @@ class DumbCohortFlow {
139
144
  continue;
140
145
  }
141
146
 
142
- // --- User is in cohort, run logic ---
143
-
144
147
  // 4a. RUN ASSET FLOW LOGIC
145
148
  const yesterdayValues = this._sumAssetValue(pYesterday.AggregatedPositions);
146
149
  const todayValues = this._sumAssetValue(pToday.AggregatedPositions);
@@ -155,15 +158,13 @@ class DumbCohortFlow {
155
158
  // 4b. RUN SECTOR ROTATION LOGIC
156
159
  this._accumulateSectorInvestment(pToday, todaySectorInvestment, sectorMap);
157
160
  this._accumulateSectorInvestment(pYesterday, yesterdaySectorInvestment, sectorMap);
158
-
159
161
  user_count++;
160
162
  }
161
163
  }
162
164
 
163
165
  logger.log('INFO', `[DumbCohortFlow] Processed ${user_count} users in cohort.`);
164
166
 
165
- // --- 5. GETRESULT LOGIC IS NOW INSIDE PROCESS ---
166
-
167
+ // --- 5. GETRESULT LOGIC ---
167
168
  if (user_count === 0) {
168
169
  logger.warn('[DumbCohortFlow] No users processed for dumb cohort. Returning null.');
169
170
  return null;
@@ -203,7 +204,6 @@ class DumbCohortFlow {
203
204
  return null;
204
205
  }
205
206
 
206
- // 6. Return combined result
207
207
  return {
208
208
  asset_flow: finalAssetFlow,
209
209
  sector_rotation: finalSectorRotation,
@@ -2,37 +2,45 @@
2
2
  * @fileoverview Calculates "Net Crowd Flow" and "Sector Rotation"
3
3
  * *only* for the "Smart Cohort" (Top 20% of Investor Scores).
4
4
  *
5
- * --- META REFACTOR ---
6
- * This calculation is now `type: "meta"` to consume in-memory dependencies.
7
- * It runs ONCE per day, receives the in-memory cache, and must
8
- * perform its own user data streaming.
5
+ * --- META REFACTOR (v2) ---
6
+ * This calculation is `type: "meta"` and expects its dependencies
7
+ * (the user-investment-profile results) to be fetched by the pass runner.
8
+ * It then streams root portfolio data.
9
9
  */
10
10
 
11
11
  const { Firestore } = require('@google-cloud/firestore');
12
12
  const firestore = new Firestore();
13
13
  const { loadAllPriceData, getDailyPriceChange } = require('../../../utils/price_data_provider');
14
14
  const { loadInstrumentMappings, getInstrumentSectorMap } = require('../../../utils/sector_mapping_provider');
15
+ // NOTE: Corrected relative path for data_loader
15
16
  const { loadDataByRefs, getPortfolioPartRefs, loadFullDayMap } = require('../../../../bulltrackers-module/functions/computation-system/utils/data_loader');
16
17
 
17
18
  const COHORT_PERCENTILE = 0.8; // Top 20%
18
19
  const PROFILE_CALC_ID = 'user-investment-profile'; // The calc to read IS scores from
19
20
 
20
21
  class SmartCohortFlow {
22
+
23
+ /**
24
+ * (NEW) Statically declare dependencies.
25
+ */
26
+ static getDependencies() {
27
+ return [PROFILE_CALC_ID];
28
+ }
29
+
21
30
  constructor() {
22
31
  // Meta-calc, no constructor state needed
23
32
  }
24
33
 
25
34
  /**
26
- * Loads the Investor Scores, calculates the cohort threshold, and builds the Set of user IDs.
27
- * --- MODIFIED: Reads from in-memory 'computedDependencies' ---
35
+ * Loads the Investor Scores from the fetched dependency.
28
36
  */
29
- _loadCohort(logger, computedDependencies) {
30
- logger.log('INFO', '[SmartCohortFlow] Loading Investor Scores from in-memory cache...');
37
+ _loadCohort(logger, fetchedDependencies) {
38
+ logger.log('INFO', '[SmartCohortFlow] Loading Investor Scores from fetched dependency...');
31
39
 
32
- const profileData = computedDependencies[PROFILE_CALC_ID];
40
+ const profileData = fetchedDependencies[PROFILE_CALC_ID];
33
41
 
34
42
  if (!profileData || !profileData.daily_investor_scores || Object.keys(profileData.daily_investor_scores).length === 0) {
35
- logger.log('WARN', `[SmartCohortFlow] Cannot find dependency in-memory: ${PROFILE_CALC_ID}. Cohort will not be built.`);
43
+ logger.log('WARN', `[SmartCohortFlow] Cannot find dependency in fetched data: ${PROFILE_CALC_ID}. Cohort will not be built.`);
36
44
  return null; // Return null to signal failure
37
45
  }
38
46
 
@@ -67,7 +75,6 @@ class SmartCohortFlow {
67
75
  }
68
76
  return valueMap;
69
77
  }
70
- // --- Sector Rotation Helper (unchanged) ---
71
78
  _accumulateSectorInvestment(portfolio, target, sectorMap) {
72
79
  if (portfolio && portfolio.AggregatedPositions) {
73
80
  for (const pos of portfolio.AggregatedPositions) {
@@ -78,16 +85,20 @@ class SmartCohortFlow {
78
85
  }
79
86
 
80
87
  /**
81
- * PROCESS: META REFACTOR
82
- * This now runs ONCE, loads all data, streams users, and returns one big result.
88
+ * REFACTORED PROCESS METHOD
89
+ * @param {string} dateStr The date to run the analysis for (e.g., "2025-10-31").
90
+ * @param {object} dependencies The shared dependencies (db, logger, rootData, etc.).
91
+ * @param {object} config The computation system configuration.
92
+ * @param {object} fetchedDependencies In-memory results from previous passes.
93
+ * @returns {Promise<object|null>} The analysis result or null.
83
94
  */
84
- async process(dateStr, dependencies, config, computedDependencies) {
95
+ async process(dateStr, dependencies, config, fetchedDependencies) {
85
96
  const { logger, db, rootData, calculationUtils } = dependencies;
86
97
  const { portfolioRefs } = rootData;
87
98
  logger.log('INFO', '[SmartCohortFlow] Starting meta-process...');
88
99
 
89
100
  // 1. Load Cohort from in-memory dependency
90
- const smartCohortIds = this._loadCohort(logger, computedDependencies);
101
+ const smartCohortIds = this._loadCohort(logger, fetchedDependencies);
91
102
  if (!smartCohortIds) {
92
103
  return null; // Dependency failed
93
104
  }
@@ -100,7 +111,7 @@ class SmartCohortFlow {
100
111
  ]);
101
112
  if (!priceMap || !mappings || !sectorMap || Object.keys(priceMap).length === 0) {
102
113
  logger.log('ERROR', '[SmartCohortFlow] Failed to load critical price/mapping/sector data. Aborting.');
103
- return null; // Return null to trigger backfill
114
+ return null;
104
115
  }
105
116
 
106
117
  // 3. Load "yesterday's" portfolio data for comparison
@@ -112,13 +123,10 @@ class SmartCohortFlow {
112
123
  logger.log('INFO', `[SmartCohortFlow] Loaded ${yesterdayRefs.length} part refs for yesterday.`);
113
124
 
114
125
  // 4. Stream "today's" portfolio data and process
115
-
116
- // --- Local state for this run ---
117
126
  const asset_values = {};
118
127
  const todaySectorInvestment = {};
119
128
  const yesterdaySectorInvestment = {};
120
129
  let user_count = 0;
121
- // --- End Local state ---
122
130
 
123
131
  const batchSize = config.partRefBatchSize || 10;
124
132
  for (let i = 0; i < portfolioRefs.length; i += batchSize) {
@@ -127,10 +135,7 @@ class SmartCohortFlow {
127
135
 
128
136
  for (const uid in todayPortfoliosChunk) {
129
137
 
130
- // --- Filter user ---
131
- if (!smartCohortIds.has(uid)) {
132
- continue;
133
- }
138
+ if (!smartCohortIds.has(uid)) continue; // --- Filter user ---
134
139
 
135
140
  const pToday = todayPortfoliosChunk[uid];
136
141
  const pYesterday = yesterdayPortfolios[uid];
@@ -139,8 +144,6 @@ class SmartCohortFlow {
139
144
  continue;
140
145
  }
141
146
 
142
- // --- User is in cohort, run logic ---
143
-
144
147
  // 4a. RUN ASSET FLOW LOGIC
145
148
  const yesterdayValues = this._sumAssetValue(pYesterday.AggregatedPositions);
146
149
  const todayValues = this._sumAssetValue(pToday.AggregatedPositions);
@@ -155,15 +158,13 @@ class SmartCohortFlow {
155
158
  // 4b. RUN SECTOR ROTATION LOGIC
156
159
  this._accumulateSectorInvestment(pToday, todaySectorInvestment, sectorMap);
157
160
  this._accumulateSectorInvestment(pYesterday, yesterdaySectorInvestment, sectorMap);
158
-
159
161
  user_count++;
160
162
  }
161
163
  }
162
164
 
163
165
  logger.log('INFO', `[SmartCohortFlow] Processed ${user_count} users in cohort.`);
164
166
 
165
- // --- 5. GETRESULT LOGIC IS NOW INSIDE PROCESS ---
166
-
167
+ // --- 5. GETRESULT LOGIC ---
167
168
  if (user_count === 0) {
168
169
  logger.warn('[SmartCohortFlow] No users processed for smart cohort. Returning null.');
169
170
  return null;
@@ -203,7 +204,6 @@ class SmartCohortFlow {
203
204
  return null;
204
205
  }
205
206
 
206
- // 6. Return combined result
207
207
  return {
208
208
  asset_flow: finalAssetFlow,
209
209
  sector_rotation: finalSectorRotation,