aiden-shared-calculations-unified 1.0.43 → 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 +40 -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,33 +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');
|
|
13
|
+
const { FieldPath } = require('@google-cloud/firestore');
|
|
29
14
|
|
|
30
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
|
+
|
|
31
26
|
constructor() {
|
|
32
27
|
this.INITIAL_CASH = 100000;
|
|
33
28
|
this.TRADE_SIZE_USD = 5000;
|
|
@@ -47,7 +42,7 @@ class StrategyPerformance {
|
|
|
47
42
|
async _findSignalInceptionDate(db, collection, computation, category) {
|
|
48
43
|
const snapshot = await db.collection(collection)
|
|
49
44
|
.where(`${category}.${computation}`, '==', true)
|
|
50
|
-
.orderBy(
|
|
45
|
+
.orderBy(FieldPath.documentId(), 'asc')
|
|
51
46
|
.limit(1)
|
|
52
47
|
.get();
|
|
53
48
|
if (snapshot.empty) return null;
|
|
@@ -61,7 +56,9 @@ class StrategyPerformance {
|
|
|
61
56
|
for (const computation in this.strategySignals) {
|
|
62
57
|
const key = `${date}_${computation}`;
|
|
63
58
|
let category = 'meta';
|
|
64
|
-
|
|
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';
|
|
65
62
|
|
|
66
63
|
const docRef = db.collection(collection).doc(date)
|
|
67
64
|
.collection(resultsSub).doc(category)
|
|
@@ -77,45 +74,45 @@ class StrategyPerformance {
|
|
|
77
74
|
}
|
|
78
75
|
|
|
79
76
|
_findInstrumentId(ticker) {
|
|
80
|
-
// 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.
|
|
81
80
|
for (const instrumentId in this.priceMap) {
|
|
82
|
-
// Note: Your price map structure may vary. This assumes a nested ticker.
|
|
83
|
-
// If priceMap is { "123": { "2023-01-01": 150 } }
|
|
84
|
-
// and you need a ticker mapping, this logic is flawed.
|
|
85
|
-
// Assuming priceMap contains ticker info, which it might not.
|
|
86
|
-
// This highlights a flaw in the original calculation.
|
|
87
|
-
// For now, we will assume it works or fails gracefully.
|
|
88
81
|
const priceData = this.priceMap[instrumentId];
|
|
89
82
|
if (priceData && priceData.ticker && priceData.ticker === ticker) {
|
|
90
83
|
return instrumentId;
|
|
91
84
|
}
|
|
92
85
|
}
|
|
86
|
+
// Fallback: assume ticker is ID (will fail for string tickers)
|
|
87
|
+
if (this.priceMap[ticker]) return ticker;
|
|
93
88
|
return null;
|
|
94
89
|
}
|
|
95
90
|
|
|
96
91
|
|
|
97
92
|
/**
|
|
93
|
+
* REFACTORED PROCESS METHOD
|
|
98
94
|
* @param {string} dateStr - Today's date.
|
|
99
95
|
* @param {object} dependencies - db, logger.
|
|
100
96
|
* @param {object} config - Computation config.
|
|
101
|
-
* @param {object}
|
|
97
|
+
* @param {object} fetchedDependencies - (UNUSED) In-memory results.
|
|
102
98
|
*/
|
|
103
|
-
async process(dateStr, dependencies, config,
|
|
104
|
-
const { db, logger } = dependencies;
|
|
99
|
+
async process(dateStr, dependencies, config, fetchedDependencies) {
|
|
100
|
+
const { db, logger, calculationUtils } = dependencies;
|
|
105
101
|
const { resultsCollection, resultsSubcollection, computationsSubcollection } = config;
|
|
106
102
|
|
|
107
103
|
// 1. Load Price Data
|
|
108
104
|
if (!this.priceMap) {
|
|
109
105
|
logger.log('INFO', '[Backtest] Loading all price data for simulation...');
|
|
110
|
-
|
|
106
|
+
// Use the utility from dependencies
|
|
107
|
+
this.priceMap = await calculationUtils.loadAllPriceData();
|
|
111
108
|
}
|
|
112
109
|
|
|
113
|
-
// 2. Find Backtest Start Date
|
|
110
|
+
// 2. Find Backtest Start Date (by finding first-ever signal)
|
|
114
111
|
const inceptionDateStr = await this._findSignalInceptionDate(
|
|
115
112
|
db,
|
|
116
113
|
resultsCollection,
|
|
117
114
|
'smart-dumb-divergence-index',
|
|
118
|
-
'meta'
|
|
115
|
+
'meta' // As defined in manifest
|
|
119
116
|
);
|
|
120
117
|
|
|
121
118
|
if (!inceptionDateStr) {
|
|
@@ -158,7 +155,7 @@ class StrategyPerformance {
|
|
|
158
155
|
pos.marketValue = price * pos.shares;
|
|
159
156
|
portfolioValue += pos.marketValue;
|
|
160
157
|
} else {
|
|
161
|
-
portfolioValue += pos.marketValue;
|
|
158
|
+
portfolioValue += pos.marketValue; // Use last known value if price missing
|
|
162
159
|
}
|
|
163
160
|
}
|
|
164
161
|
history.push({ date, portfolioValue });
|
|
@@ -170,8 +167,7 @@ class StrategyPerformance {
|
|
|
170
167
|
if (!signalData) continue;
|
|
171
168
|
|
|
172
169
|
const signalRules = this.strategySignals[computation];
|
|
173
|
-
|
|
174
|
-
const assetSignals = signalData.assets || signalData; // Handle both structures
|
|
170
|
+
const assetSignals = signalData.assets || signalData;
|
|
175
171
|
|
|
176
172
|
for (const ticker in assetSignals) {
|
|
177
173
|
const signal = assetSignals[ticker]?.status;
|
|
@@ -184,8 +180,8 @@ class StrategyPerformance {
|
|
|
184
180
|
// C. Execute Trades
|
|
185
181
|
for (const ticker in tradesToMake) {
|
|
186
182
|
const action = tradesToMake[ticker];
|
|
187
|
-
|
|
188
|
-
|
|
183
|
+
// HACK: Use the (flawed) original logic to find ID
|
|
184
|
+
const instrumentId = this._findInstrumentId(ticker) || ticker;
|
|
189
185
|
|
|
190
186
|
const price = this.priceMap[instrumentId]?.[date];
|
|
191
187
|
if (!price || price <= 0) continue;
|
|
@@ -212,14 +208,12 @@ class StrategyPerformance {
|
|
|
212
208
|
|
|
213
209
|
logger.log('INFO', `[Backtest] Simulation complete. Final Value: ${finalValue}, Return: ${totalReturnPct.toFixed(2)}%`);
|
|
214
210
|
|
|
215
|
-
// We only save the *final* results, not the daily history (which is large)
|
|
216
211
|
return {
|
|
217
212
|
strategyName: 'SmartDumbDivergence_v1',
|
|
218
213
|
inceptionDate: inceptionDateStr,
|
|
219
214
|
endDate: dateStr,
|
|
220
215
|
finalPortfolioValue: finalValue,
|
|
221
216
|
totalReturnPercent: totalReturnPct,
|
|
222
|
-
// dailyHistory: history // <-- Too large for a single doc
|
|
223
217
|
};
|
|
224
218
|
}
|
|
225
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,
|