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.
- package/calculations/backtests/strategy-performance.js +39 -46
- package/calculations/behavioural/historical/dumb-cohort-flow.js +29 -29
- package/calculations/behavioural/historical/smart-cohort-flow.js +29 -29
- package/calculations/behavioural/historical/smart_money_flow.js +108 -104
- package/calculations/behavioural/historical/user-investment-profile.js +51 -95
- package/calculations/meta/capital_deployment_strategy.js +25 -15
- package/calculations/meta/capital_liquidation_performance.js +18 -4
- package/calculations/meta/capital_vintage_performance.js +18 -4
- package/calculations/meta/cash-flow-deployment.js +42 -16
- package/calculations/meta/cash-flow-liquidation.js +27 -15
- package/calculations/meta/profit_cohort_divergence.js +22 -9
- package/calculations/meta/smart-dumb-divergence-index.js +19 -9
- package/calculations/meta/social_flow_correlation.js +20 -8
- package/calculations/pnl/pnl_distribution_per_stock.js +75 -37
- package/package.json +1 -1
|
@@ -1,34 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Backtest (Pass
|
|
3
|
-
* Runs a full historical simulation of a trading strategy
|
|
4
|
-
*
|
|
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');
|
|
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
|
-
|
|
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
|
|
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}
|
|
97
|
+
* @param {object} fetchedDependencies - (UNUSED) In-memory results.
|
|
103
98
|
*/
|
|
104
|
-
async process(dateStr, dependencies, config,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
189
|
-
|
|
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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
27
|
-
* --- MODIFIED: Reads from in-memory 'computedDependencies' ---
|
|
35
|
+
* Loads the Investor Scores from the fetched dependency.
|
|
28
36
|
*/
|
|
29
|
-
_loadCohort(logger,
|
|
30
|
-
logger.log('INFO', '[DumbCohortFlow] Loading Investor Scores from
|
|
37
|
+
_loadCohort(logger, fetchedDependencies) {
|
|
38
|
+
logger.log('INFO', '[DumbCohortFlow] Loading Investor Scores from fetched dependency...');
|
|
31
39
|
|
|
32
|
-
const profileData =
|
|
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
|
|
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
|
|
82
|
-
*
|
|
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,
|
|
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,
|
|
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;
|
|
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
|
|
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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
27
|
-
* --- MODIFIED: Reads from in-memory 'computedDependencies' ---
|
|
35
|
+
* Loads the Investor Scores from the fetched dependency.
|
|
28
36
|
*/
|
|
29
|
-
_loadCohort(logger,
|
|
30
|
-
logger.log('INFO', '[SmartCohortFlow] Loading Investor Scores from
|
|
37
|
+
_loadCohort(logger, fetchedDependencies) {
|
|
38
|
+
logger.log('INFO', '[SmartCohortFlow] Loading Investor Scores from fetched dependency...');
|
|
31
39
|
|
|
32
|
-
const profileData =
|
|
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
|
|
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
|
|
82
|
-
*
|
|
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,
|
|
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,
|
|
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;
|
|
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
|
|
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,
|