aiden-shared-calculations-unified 1.0.44 → 1.0.46
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 +30 -17
- package/package.json +1 -1
|
@@ -1,64 +1,128 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Tracks the investment flow of "smart money".
|
|
3
3
|
*
|
|
4
|
-
* ---
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* before user_profitability_tracker has *written* them.
|
|
9
|
-
* --- END FIX ---
|
|
4
|
+
* --- META REFACTOR (v2) ---
|
|
5
|
+
* This is a streaming meta-calc. It reads historical shards (a side-effect
|
|
6
|
+
* from Pass 1) and streams root portfolio data to calculate its result.
|
|
7
|
+
* It has no direct computational dependencies passed to `process`.
|
|
10
8
|
*/
|
|
11
9
|
|
|
12
10
|
const { Firestore } = require('@google-cloud/firestore');
|
|
13
11
|
const firestore = new Firestore();
|
|
14
|
-
// CORRECTED PATH: ../utils/ instead of ../../utils/
|
|
15
12
|
const { getInstrumentSectorMap } = require('../../../utils/sector_mapping_provider');
|
|
13
|
+
// NOTE: Corrected relative path for data_loader
|
|
14
|
+
const { loadDataByRefs, getPortfolioPartRefs, loadFullDayMap } = require('../../../../bulltrackers-module/functions/computation-system/utils/data_loader');
|
|
15
|
+
|
|
16
16
|
|
|
17
17
|
class SmartMoneyFlow {
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* (NEW) Statically declare dependencies.
|
|
21
|
+
* This calc reads shards from Pass 1, but doesn't take a computed
|
|
22
|
+
* result as an argument. The manifest ensures it runs in Pass 2.
|
|
23
|
+
*/
|
|
24
|
+
static getDependencies() {
|
|
25
|
+
return ['user-profitability-tracker']; // This is just for ordering, not for `fetchedDependencies`
|
|
26
|
+
}
|
|
27
|
+
|
|
18
28
|
constructor() {
|
|
19
29
|
this.smartMoneyUsers = new Set();
|
|
20
|
-
this.sectorMap = null;
|
|
21
|
-
|
|
22
|
-
// --- NEW ---
|
|
30
|
+
this.sectorMap = null;
|
|
31
|
+
|
|
23
32
|
// We now store the portfolio data during process()
|
|
33
|
+
// (This state is reset for each date by the runner)
|
|
24
34
|
this.todayPortfolios = {};
|
|
25
35
|
this.yesterdayPortfolios = {};
|
|
26
|
-
// --- END NEW ---
|
|
27
36
|
}
|
|
28
37
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
/**
|
|
39
|
+
* REFACTORED PROCESS METHOD
|
|
40
|
+
* This is now the *first* method called by the runner.
|
|
41
|
+
* It caches the portfolios.
|
|
42
|
+
*/
|
|
43
|
+
async process(dateStr, dependencies, config, fetchedDependencies) {
|
|
44
|
+
const { logger, db, rootData } = dependencies;
|
|
45
|
+
const { portfolioRefs } = rootData;
|
|
46
|
+
logger.log('INFO', '[SmartMoneyFlow] Starting meta-process...');
|
|
47
|
+
|
|
48
|
+
// 1. Load external dependencies (sectors)
|
|
34
49
|
if (!this.sectorMap) {
|
|
35
50
|
try {
|
|
36
51
|
this.sectorMap = await getInstrumentSectorMap();
|
|
37
|
-
if (!this.sectorMap || Object.keys(this.sectorMap).length === 0) {
|
|
38
|
-
console.warn('Sector map loaded but is empty.');
|
|
39
|
-
}
|
|
40
52
|
} catch (error) {
|
|
41
|
-
|
|
42
|
-
return; //
|
|
53
|
+
logger.log('ERROR', '[SmartMoneyFlow] Failed to load sector map.', { err: error.message });
|
|
54
|
+
return null; // Abort
|
|
43
55
|
}
|
|
44
56
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
57
|
+
|
|
58
|
+
// 2. Load "yesterday's" portfolio data for comparison
|
|
59
|
+
const yesterdayDate = new Date(dateStr + 'T00:00:00Z');
|
|
60
|
+
yesterdayDate.setUTCDate(yesterdayDate.getUTCDate() - 1);
|
|
61
|
+
const yesterdayStr = yesterdayDate.toISOString().slice(0, 10);
|
|
62
|
+
const yesterdayRefs = await getPortfolioPartRefs(config, dependencies, yesterdayStr);
|
|
63
|
+
const yesterdayPortfolios = await loadFullDayMap(config, dependencies, yesterdayRefs);
|
|
64
|
+
logger.log('INFO', `[SmartMoneyFlow] Loaded ${yesterdayRefs.length} part refs for yesterday.`);
|
|
65
|
+
|
|
66
|
+
// 3. Identify smart money users by reading Pass 1 shards
|
|
67
|
+
// This is the dependency.
|
|
68
|
+
await this.identifySmartMoney(dependencies);
|
|
69
|
+
if (this.smartMoneyUsers.size === 0) {
|
|
70
|
+
logger.log('WARN', '[SmartMoneyFlow] No smart money users identified. Aborting.');
|
|
71
|
+
return { smart_money_flow: {} };
|
|
48
72
|
}
|
|
49
|
-
|
|
50
|
-
|
|
73
|
+
|
|
74
|
+
// 4. Stream "today's" portfolio data and process *only smart users*
|
|
75
|
+
const batchSize = config.partRefBatchSize || 10;
|
|
76
|
+
const sectorFlow = {};
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < portfolioRefs.length; i += batchSize) {
|
|
79
|
+
const batchRefs = portfolioRefs.slice(i, i + batchSize);
|
|
80
|
+
const todayPortfoliosChunk = await loadDataByRefs(config, dependencies, batchRefs);
|
|
81
|
+
|
|
82
|
+
for (const uid in todayPortfoliosChunk) {
|
|
83
|
+
// --- Filter user ---
|
|
84
|
+
if (!this.smartMoneyUsers.has(uid)) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const pToday = todayPortfoliosChunk[uid];
|
|
89
|
+
const pYesterday = yesterdayPortfolios[uid];
|
|
90
|
+
|
|
91
|
+
if (!pToday || !pYesterday) continue;
|
|
92
|
+
|
|
93
|
+
// --- User is in cohort, run logic ---
|
|
94
|
+
const todaySectorInvestment = this.calculateSectorInvestment(pToday);
|
|
95
|
+
const yesterdaySectorInvestment = this.calculateSectorInvestment(pYesterday);
|
|
96
|
+
|
|
97
|
+
const allSectors = new Set([...Object.keys(todaySectorInvestment), ...Object.keys(yesterdaySectorInvestment)]);
|
|
98
|
+
for (const sector of allSectors) {
|
|
99
|
+
const todayAmount = todaySectorInvestment[sector] || 0;
|
|
100
|
+
const yesterdayAmount = yesterdaySectorInvestment[sector] || 0;
|
|
101
|
+
const change = todayAmount - yesterdayAmount;
|
|
102
|
+
|
|
103
|
+
if (change !== 0) {
|
|
104
|
+
if (!sectorFlow[sector]) {
|
|
105
|
+
sectorFlow[sector] = 0;
|
|
106
|
+
}
|
|
107
|
+
sectorFlow[sector] += change;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
51
111
|
}
|
|
112
|
+
|
|
113
|
+
logger.log('INFO', `[SmartMoneyFlow] Processed ${this.smartMoneyUsers.size} smart users.`);
|
|
114
|
+
|
|
115
|
+
// 5. Return final result
|
|
116
|
+
return { smart_money_flow: sectorFlow };
|
|
52
117
|
}
|
|
53
118
|
|
|
54
|
-
async identifySmartMoney() {
|
|
55
|
-
|
|
119
|
+
async identifySmartMoney(dependencies) {
|
|
120
|
+
const { logger } = dependencies;
|
|
121
|
+
logger.log('INFO', "[SmartMoneyFlow] Attempting to identify smart money users by reading shards...");
|
|
56
122
|
try {
|
|
57
|
-
// Fetching sharded profitability data
|
|
58
123
|
const shardPromises = [];
|
|
59
|
-
const NUM_SHARDS = 50;
|
|
124
|
+
const NUM_SHARDS = 50;
|
|
60
125
|
for (let i = 0; i < NUM_SHARDS; i++) {
|
|
61
|
-
// Corrected document path for shards
|
|
62
126
|
const docRef = firestore.collection('historical_insights').doc(`user_profitability_shard_${i}`);
|
|
63
127
|
shardPromises.push(docRef.get());
|
|
64
128
|
}
|
|
@@ -70,10 +134,7 @@ class SmartMoneyFlow {
|
|
|
70
134
|
const shardData = snap.data() || {};
|
|
71
135
|
for (const userId in shardData) {
|
|
72
136
|
const history = shardData[userId];
|
|
73
|
-
// Check if history is an array and has enough entries
|
|
74
137
|
if (Array.isArray(history) && history.length >= 5) {
|
|
75
|
-
// "Smart money" definition: profitable for at least 5 of the last 7 days
|
|
76
|
-
// Filter based on the 'pnl' property of each history entry
|
|
77
138
|
const profitableDays = history.slice(-7).filter(d => d && typeof d.pnl === 'number' && d.pnl > 0).length;
|
|
78
139
|
if (profitableDays >= 5) {
|
|
79
140
|
this.smartMoneyUsers.add(userId);
|
|
@@ -82,32 +143,25 @@ class SmartMoneyFlow {
|
|
|
82
143
|
}
|
|
83
144
|
}
|
|
84
145
|
} else {
|
|
85
|
-
|
|
146
|
+
logger.log('WARN', `[SmartMoneyFlow] Profitability shard user_profitability_shard_${index} does not exist.`);
|
|
86
147
|
}
|
|
87
148
|
});
|
|
88
|
-
|
|
149
|
+
logger.log('INFO', `[SmartMoneyFlow] Found ${profitableUsersFound} smart money users across all shards.`);
|
|
89
150
|
|
|
90
151
|
} catch (error) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
this.smartMoneyUsers.clear(); // Ensure set is empty on error
|
|
94
|
-
}
|
|
95
|
-
// Final check after attempt
|
|
96
|
-
if (this.smartMoneyUsers.size === 0) {
|
|
97
|
-
console.warn("No smart money users identified. Smart money flow calculation might be empty.");
|
|
152
|
+
logger.log('ERROR', '[SmartMoneyFlow] Error identifying smart money users', { err: error.message });
|
|
153
|
+
this.smartMoneyUsers.clear();
|
|
98
154
|
}
|
|
99
155
|
}
|
|
100
156
|
|
|
101
157
|
|
|
102
158
|
calculateSectorInvestment(portfolio) {
|
|
103
159
|
const sectorInvestment = {};
|
|
104
|
-
// Ensure sectorMap is loaded
|
|
105
160
|
if (!this.sectorMap) {
|
|
106
|
-
console.warn("Cannot calculate sector investment: Sector map not loaded.");
|
|
107
|
-
return sectorInvestment;
|
|
161
|
+
console.warn("[SmartMoneyFlow] Cannot calculate sector investment: Sector map not loaded.");
|
|
162
|
+
return sectorInvestment;
|
|
108
163
|
}
|
|
109
164
|
|
|
110
|
-
// Use AggregatedPositions as it contains 'Invested' amount
|
|
111
165
|
if (portfolio && portfolio.AggregatedPositions && Array.isArray(portfolio.AggregatedPositions)) {
|
|
112
166
|
for (const pos of portfolio.AggregatedPositions) {
|
|
113
167
|
if (pos && typeof pos.InstrumentID !== 'undefined' && typeof pos.Invested === 'number') {
|
|
@@ -116,70 +170,20 @@ class SmartMoneyFlow {
|
|
|
116
170
|
sectorInvestment[sector] = 0;
|
|
117
171
|
}
|
|
118
172
|
sectorInvestment[sector] += pos.Invested;
|
|
119
|
-
} else {
|
|
120
|
-
// Log potentially malformed position data
|
|
121
|
-
// console.warn('Skipping position due to missing InstrumentID or Invested amount:', pos);
|
|
122
173
|
}
|
|
123
174
|
}
|
|
124
|
-
} else {
|
|
125
|
-
// Log if AggregatedPositions are missing or not an array
|
|
126
|
-
// console.warn('AggregatedPositions missing or invalid in portfolio for sector investment calculation.');
|
|
127
175
|
}
|
|
128
176
|
return sectorInvestment;
|
|
129
177
|
}
|
|
130
178
|
|
|
131
179
|
|
|
132
|
-
async getResult() {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (!this.sectorMap) {
|
|
139
|
-
console.error('SmartMoneyFlow: Sector map not loaded, cannot get result.');
|
|
140
|
-
return { smart_money_flow: {} };
|
|
141
|
-
}
|
|
142
|
-
// --- END FIX ---
|
|
143
|
-
|
|
144
|
-
const sectorFlow = {};
|
|
145
|
-
|
|
146
|
-
// 3. Loop through the cached portfolios
|
|
147
|
-
for (const userId of this.smartMoneyUsers) {
|
|
148
|
-
const todayPortfolio = this.todayPortfolios[userId];
|
|
149
|
-
const yesterdayPortfolio = this.yesterdayPortfolios[userId];
|
|
150
|
-
|
|
151
|
-
if (todayPortfolio && yesterdayPortfolio) {
|
|
152
|
-
const todaySectorInvestment = this.calculateSectorInvestment(todayPortfolio);
|
|
153
|
-
const yesterdaySectorInvestment = this.calculateSectorInvestment(yesterdayPortfolio);
|
|
154
|
-
|
|
155
|
-
// Calculate change in investment per sector
|
|
156
|
-
const allSectors = new Set([...Object.keys(todaySectorInvestment), ...Object.keys(yesterdaySectorInvestment)]);
|
|
157
|
-
for (const sector of allSectors) {
|
|
158
|
-
const todayAmount = todaySectorInvestment[sector] || 0;
|
|
159
|
-
const yesterdayAmount = yesterdaySectorInvestment[sector] || 0;
|
|
160
|
-
const change = todayAmount - yesterdayAmount;
|
|
161
|
-
|
|
162
|
-
// Only record if there is a change
|
|
163
|
-
if (change !== 0) {
|
|
164
|
-
if (!sectorFlow[sector]) {
|
|
165
|
-
sectorFlow[sector] = 0;
|
|
166
|
-
}
|
|
167
|
-
sectorFlow[sector] += change;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Return only sectors with non-zero flow if desired, or the full object
|
|
174
|
-
const filteredFlow = {};
|
|
175
|
-
for (const sector in sectorFlow) {
|
|
176
|
-
if (sectorFlow[sector] !== 0) {
|
|
177
|
-
filteredFlow[sector] = sectorFlow[sector];
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
console.log("Final Smart Money Flow:", filteredFlow);
|
|
181
|
-
return { smart_money_flow: filteredFlow };
|
|
180
|
+
async getResult() { return null; }
|
|
181
|
+
reset() {
|
|
182
|
+
this.smartMoneyUsers.clear();
|
|
183
|
+
this.sectorMap = null;
|
|
184
|
+
this.todayPortfolios = {};
|
|
185
|
+
this.yesterdayPortfolios = {};
|
|
182
186
|
}
|
|
183
187
|
}
|
|
184
188
|
|
|
185
|
-
module.exports = SmartMoneyFlow;
|
|
189
|
+
module.exports = SmartMoneyFlow;
|
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Calculates a rolling 90-day "Investor Score" (IS) for each normal user.
|
|
3
3
|
*
|
|
4
|
-
* --- META REFACTOR ---
|
|
5
|
-
* This calculation is now `type: "meta"`
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* --- META REFACTOR (v2) ---
|
|
5
|
+
* This calculation is now `type: "meta"` and expects its dependencies
|
|
6
|
+
* to be fetched by the pass runner and provided to its `process` method.
|
|
7
|
+
* It also dynamically fetches its own historical user data.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const { Firestore } = require('@google-cloud/firestore');
|
|
11
11
|
const firestore = new Firestore();
|
|
12
|
-
|
|
13
|
-
const {
|
|
14
|
-
const {
|
|
15
|
-
|
|
12
|
+
// NOTE: Corrected relative path for data_loader
|
|
13
|
+
const { loadDataByRefs, getPortfolioPartRefs, loadFullDayMap } = require('../../../../bulltrackers-module/functions/computation-system/utils/data_loader');
|
|
14
|
+
const { loadAllPriceData, getInstrumentSectorMap } = require('../../../utils/price_data_provider');
|
|
15
|
+
|
|
16
|
+
// Config
|
|
17
|
+
const NUM_SHARDS = 50;
|
|
16
18
|
const ROLLING_DAYS = 90;
|
|
17
|
-
const SHARD_COLLECTION_NAME = 'user_profile_history';
|
|
18
|
-
const PNL_TRACKER_CALC_ID = 'user-profitability-tracker';
|
|
19
|
+
const SHARD_COLLECTION_NAME = 'user_profile_history';
|
|
20
|
+
const PNL_TRACKER_CALC_ID = 'user-profitability-tracker';
|
|
19
21
|
|
|
20
|
-
// Helper: stable shard index
|
|
22
|
+
// Helper: stable shard index
|
|
21
23
|
function getShardIndex(id) {
|
|
22
24
|
const n = parseInt(id, 10);
|
|
23
25
|
if (!Number.isNaN(n)) return Math.abs(n) % NUM_SHARDS;
|
|
@@ -30,16 +32,15 @@ function getShardIndex(id) {
|
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
class UserInvestmentProfile {
|
|
33
|
-
constructor() {
|
|
34
|
-
// --- META REFACTOR ---
|
|
35
|
-
// All state is now managed inside the `process` function.
|
|
36
|
-
// The constructor, getResult, and reset methods are no longer used
|
|
37
|
-
// by the meta-runner, but we leave them for compatibility.
|
|
38
|
-
// --- END REFACTOR ---
|
|
39
|
-
}
|
|
40
35
|
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
/**
|
|
37
|
+
* (NEW) Statically declare dependencies.
|
|
38
|
+
* The runner will fetch these results for the *current date*
|
|
39
|
+
* and pass them to `process`.
|
|
40
|
+
*/
|
|
41
|
+
static getDependencies() {
|
|
42
|
+
return [PNL_TRACKER_CALC_ID];
|
|
43
|
+
}
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
46
|
* HEURISTIC 1: Risk & Diversification Score (0-10).
|
|
@@ -59,7 +60,7 @@ class UserInvestmentProfile {
|
|
|
59
60
|
for (const pos of positions) {
|
|
60
61
|
const invested = pos.InvestedAmount || pos.Amount || 0;
|
|
61
62
|
const netProfit = ('NetProfit' in pos) ? pos.NetProfit : (pos.ProfitAndLoss || 0); // decimal % return
|
|
62
|
-
const ret = invested > 0 ? (netProfit) : netProfit;
|
|
63
|
+
const ret = invested > 0 ? (netProfit) : netProfit;
|
|
63
64
|
|
|
64
65
|
weightedRetSum += ret * invested;
|
|
65
66
|
weightedRetSqSum += (ret * ret) * invested;
|
|
@@ -69,29 +70,19 @@ class UserInvestmentProfile {
|
|
|
69
70
|
sectors.add(sectorMap[pos.InstrumentID] || 'N/A');
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
// Weighted mean & variance of returns
|
|
73
73
|
const meanReturn = totalInvested > 0 ? (weightedRetSum / totalInvested) : 0;
|
|
74
74
|
const meanReturnSq = totalInvested > 0 ? (weightedRetSqSum / totalInvested) : (meanReturn * meanReturn);
|
|
75
75
|
const variance = Math.max(0, meanReturnSq - (meanReturn * meanReturn));
|
|
76
76
|
const stdReturn = Math.sqrt(variance);
|
|
77
|
-
|
|
78
|
-
// dispersion proxy: mean / std (if std is zero we treat as neutral 0)
|
|
79
77
|
let dispersionRiskProxy = stdReturn > 0 ? meanReturn / stdReturn : 0;
|
|
80
|
-
|
|
81
|
-
// cap and map dispersion proxy to [0..10].
|
|
82
|
-
// dispersionRiskProxy can be outside [-2..4], clamp to reasonable bounds first.
|
|
83
78
|
const capped = Math.max(-2, Math.min(4, dispersionRiskProxy));
|
|
84
|
-
const scoreSharpe = ((capped + 2) / 6) * 10;
|
|
85
|
-
|
|
86
|
-
// Sector diversification (monotonic - diminishing returns)
|
|
79
|
+
const scoreSharpe = ((capped + 2) / 6) * 10;
|
|
87
80
|
const sectorCount = sectors.size;
|
|
88
81
|
let scoreDiversification = 0;
|
|
89
82
|
if (sectorCount === 1) scoreDiversification = 0;
|
|
90
83
|
else if (sectorCount <= 4) scoreDiversification = 5;
|
|
91
84
|
else if (sectorCount <= 7) scoreDiversification = 8;
|
|
92
85
|
else scoreDiversification = 10;
|
|
93
|
-
|
|
94
|
-
// Position sizing / concentration penalty
|
|
95
86
|
const concentrationRatio = totalInvested > 0 ? (maxPosition / totalInvested) : 0;
|
|
96
87
|
let scoreSizing = 0;
|
|
97
88
|
if (concentrationRatio > 0.8) scoreSizing = 0;
|
|
@@ -99,7 +90,6 @@ class UserInvestmentProfile {
|
|
|
99
90
|
else if (concentrationRatio > 0.3) scoreSizing = 5;
|
|
100
91
|
else if (concentrationRatio > 0.15) scoreSizing = 8;
|
|
101
92
|
else scoreSizing = 10;
|
|
102
|
-
|
|
103
93
|
const final = (scoreSharpe * 0.4) + (scoreDiversification * 0.3) + (scoreSizing * 0.3);
|
|
104
94
|
return Math.max(0, Math.min(10, final));
|
|
105
95
|
}
|
|
@@ -110,42 +100,34 @@ class UserInvestmentProfile {
|
|
|
110
100
|
_calculateDisciplineScore(yesterdayPortfolio = {}, todayPortfolio = {}) {
|
|
111
101
|
const yPositions = yesterdayPortfolio.AggregatedPositions || [];
|
|
112
102
|
const tPositions = new Map((todayPortfolio.AggregatedPositions || []).map(p => [p.PositionID, p]));
|
|
113
|
-
|
|
114
103
|
if (yPositions.length === 0) {
|
|
115
|
-
return 5; // neutral
|
|
104
|
+
return 5; // neutral
|
|
116
105
|
}
|
|
117
|
-
|
|
118
106
|
let eventPoints = 0;
|
|
119
107
|
let eventCount = 0;
|
|
120
|
-
|
|
121
108
|
for (const yPos of yPositions) {
|
|
122
109
|
const profitAndLoss = ('ProfitAndLoss' in yPos) ? yPos.ProfitAndLoss : (yPos.NetProfit || 0);
|
|
123
110
|
const invested = yPos.InvestedAmount || yPos.Amount || 0;
|
|
124
|
-
const pnlPercent = profitAndLoss;
|
|
125
|
-
|
|
111
|
+
const pnlPercent = profitAndLoss;
|
|
126
112
|
const tPos = tPositions.get(yPos.PositionID);
|
|
127
|
-
|
|
128
113
|
if (!tPos) {
|
|
129
|
-
// Closed position
|
|
130
114
|
eventCount++;
|
|
131
|
-
if (pnlPercent < -0.05) eventPoints += 10;
|
|
132
|
-
else if (pnlPercent > 0.20) eventPoints += 8;
|
|
133
|
-
else if (pnlPercent > 0 && pnlPercent < 0.05) eventPoints += 2;
|
|
134
|
-
else eventPoints += 5;
|
|
115
|
+
if (pnlPercent < -0.05) eventPoints += 10;
|
|
116
|
+
else if (pnlPercent > 0.20) eventPoints += 8;
|
|
117
|
+
else if (pnlPercent > 0 && pnlPercent < 0.05) eventPoints += 2;
|
|
118
|
+
else eventPoints += 5;
|
|
135
119
|
} else {
|
|
136
|
-
// Held or modified
|
|
137
120
|
if (pnlPercent < -0.10) {
|
|
138
121
|
eventCount++;
|
|
139
122
|
const tInvested = tPos.InvestedAmount || tPos.Amount || 0;
|
|
140
|
-
if (tInvested > invested) eventPoints += 0;
|
|
141
|
-
else eventPoints += 3;
|
|
123
|
+
if (tInvested > invested) eventPoints += 0;
|
|
124
|
+
else eventPoints += 3;
|
|
142
125
|
} else if (pnlPercent > 0.15) {
|
|
143
126
|
eventCount++;
|
|
144
|
-
eventPoints += 10;
|
|
127
|
+
eventPoints += 10;
|
|
145
128
|
}
|
|
146
129
|
}
|
|
147
130
|
}
|
|
148
|
-
|
|
149
131
|
const avg = (eventCount > 0) ? (eventPoints / eventCount) : 5;
|
|
150
132
|
return Math.max(0, Math.min(10, avg));
|
|
151
133
|
}
|
|
@@ -156,44 +138,33 @@ class UserInvestmentProfile {
|
|
|
156
138
|
_calculateMarketTimingScore(yesterdayPortfolio = {}, todayPortfolio = {}, priceMap) {
|
|
157
139
|
const yIds = new Set((yesterdayPortfolio.AggregatedPositions || []).map(p => p.PositionID));
|
|
158
140
|
const newPositions = (todayPortfolio.AggregatedPositions || []).filter(p => !yIds.has(p.PositionID));
|
|
159
|
-
|
|
160
141
|
if (newPositions.length === 0) return 5;
|
|
161
|
-
|
|
162
142
|
let timingPoints = 0;
|
|
163
143
|
let timingCount = 0;
|
|
164
|
-
|
|
165
144
|
for (const tPos of newPositions) {
|
|
166
145
|
const prices = priceMap[tPos.InstrumentID];
|
|
167
146
|
if (!prices) continue;
|
|
168
|
-
|
|
169
|
-
// Accept prices as either array or {date:price} map; build sorted array of prices
|
|
170
147
|
let historyPrices = [];
|
|
171
148
|
if (Array.isArray(prices)) {
|
|
172
|
-
// assume array of numbers or objects with .price/.close
|
|
173
149
|
historyPrices = prices
|
|
174
150
|
.map(p => (typeof p === 'number' ? p : (p.price || p.close || null)))
|
|
175
151
|
.filter(v => v != null);
|
|
176
152
|
} else {
|
|
177
|
-
// object keyed by date -> price
|
|
178
153
|
const entries = Object.keys(prices)
|
|
179
154
|
.map(d => ({ d, p: prices[d] }))
|
|
180
155
|
.filter(e => e.p != null)
|
|
181
156
|
.sort((a, b) => new Date(a.d) - new Date(b.d));
|
|
182
157
|
historyPrices = entries.map(e => e.p);
|
|
183
158
|
}
|
|
184
|
-
|
|
185
159
|
const last30 = historyPrices.slice(-30);
|
|
186
160
|
if (last30.length < 2) continue;
|
|
187
|
-
|
|
188
161
|
const minPrice = Math.min(...last30);
|
|
189
162
|
const maxPrice = Math.max(...last30);
|
|
190
163
|
const openRate = tPos.OpenRate;
|
|
191
164
|
const range = maxPrice - minPrice;
|
|
192
165
|
if (!isFinite(range) || range === 0) continue;
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
proximity = Math.max(0, Math.min(1, proximity)); // clamp to [0,1]
|
|
196
|
-
|
|
166
|
+
let proximity = (openRate - minPrice) / range;
|
|
167
|
+
proximity = Math.max(0, Math.min(1, proximity));
|
|
197
168
|
timingCount++;
|
|
198
169
|
if (proximity < 0.2) timingPoints += 10;
|
|
199
170
|
else if (proximity < 0.4) timingPoints += 8;
|
|
@@ -201,30 +172,32 @@ class UserInvestmentProfile {
|
|
|
201
172
|
else if (proximity > 0.7) timingPoints += 3;
|
|
202
173
|
else timingPoints += 5;
|
|
203
174
|
}
|
|
204
|
-
|
|
205
175
|
const avg = (timingCount > 0) ? (timingPoints / timingCount) : 5;
|
|
206
176
|
return Math.max(0, Math.min(10, avg));
|
|
207
177
|
}
|
|
208
|
-
|
|
209
|
-
|
|
178
|
+
|
|
210
179
|
/**
|
|
211
|
-
* PROCESS
|
|
212
|
-
*
|
|
180
|
+
* REFACTORED PROCESS METHOD
|
|
181
|
+
* @param {string} dateStr - The date being processed (YYYY-MM-DD).
|
|
182
|
+
* @param {object} dependencies - Shared dependencies (db, logger, rootData, etc.).
|
|
183
|
+
* @param {object} config - The computation system configuration.
|
|
184
|
+
* @param {object} fetchedDependencies - An object containing the results of this
|
|
185
|
+
* calc's dependencies (e.g., { 'user-profitability-tracker': ... }).
|
|
213
186
|
*/
|
|
214
|
-
async process(dateStr, dependencies, config,
|
|
215
|
-
const { logger, db, rootData
|
|
187
|
+
async process(dateStr, dependencies, config, fetchedDependencies) {
|
|
188
|
+
const { logger, db, rootData } = dependencies;
|
|
216
189
|
const { portfolioRefs } = rootData;
|
|
217
190
|
|
|
218
191
|
logger.log('INFO', '[UserInvestmentProfile] Starting meta-process...');
|
|
219
192
|
|
|
220
|
-
// 1. Get Pass 1 dependency from
|
|
221
|
-
const pnlTrackerResult =
|
|
193
|
+
// 1. Get Pass 1 dependency from the `fetchedDependencies` argument
|
|
194
|
+
const pnlTrackerResult = fetchedDependencies[PNL_TRACKER_CALC_ID];
|
|
222
195
|
if (!pnlTrackerResult || !pnlTrackerResult.daily_pnl_map) {
|
|
223
|
-
logger.log('WARN', `[UserInvestmentProfile]
|
|
196
|
+
logger.log('WARN', `[UserInvestmentProfile] Dependency '${PNL_TRACKER_CALC_ID}' not found in fetchedDependencies. Aborting.`);
|
|
224
197
|
return null; // Return null to signal failure
|
|
225
198
|
}
|
|
226
199
|
const pnlScores = pnlTrackerResult.daily_pnl_map;
|
|
227
|
-
logger.log('INFO', `[UserInvestmentProfile] Received ${Object.keys(pnlScores).length} PNL scores
|
|
200
|
+
logger.log('INFO', `[UserInvestmentProfile] Received ${Object.keys(pnlScores).length} PNL scores from dependency.`);
|
|
228
201
|
|
|
229
202
|
// 2. Load external dependencies (prices, sectors)
|
|
230
203
|
const [priceMap, sectorMap] = await Promise.all([
|
|
@@ -254,11 +227,10 @@ class UserInvestmentProfile {
|
|
|
254
227
|
|
|
255
228
|
for (const uid in todayPortfoliosChunk) {
|
|
256
229
|
const pToday = todayPortfoliosChunk[uid];
|
|
257
|
-
if (!pToday || !pToday.AggregatedPositions) continue; // Skip
|
|
230
|
+
if (!pToday || !pToday.AggregatedPositions) continue; // Skip
|
|
258
231
|
|
|
259
232
|
const pYesterday = yesterdayPortfolios[uid] || {};
|
|
260
233
|
|
|
261
|
-
// Run the heuristic calculations
|
|
262
234
|
const score_rd = this._calculateRiskAndDivScore(pToday, sectorMap);
|
|
263
235
|
const score_disc = this._calculateDisciplineScore(pYesterday, pToday);
|
|
264
236
|
const score_time = this._calculateMarketTimingScore(pYesterday, pToday, priceMap);
|
|
@@ -272,15 +244,12 @@ class UserInvestmentProfile {
|
|
|
272
244
|
}
|
|
273
245
|
logger.log('INFO', `[UserInvestmentProfile] Calculated daily scores for ${Object.keys(dailyUserScores).length} users.`);
|
|
274
246
|
|
|
275
|
-
//
|
|
276
|
-
// (This is the logic from your original getResult())
|
|
277
|
-
|
|
247
|
+
// 5. GETRESULT LOGIC (Final aggregation)
|
|
278
248
|
const shardedResults = {};
|
|
279
249
|
for (let i = 0; i < NUM_SHARDS; i++) {
|
|
280
250
|
const shardKey = `${SHARD_COLLECTION_NAME}_shard_${i}`;
|
|
281
251
|
shardedResults[shardKey] = { profiles: {}, lastUpdated: dateStr };
|
|
282
252
|
}
|
|
283
|
-
|
|
284
253
|
const dailyInvestorScoreMap = {};
|
|
285
254
|
|
|
286
255
|
// Fetch existing shards in parallel
|
|
@@ -296,16 +265,13 @@ class UserInvestmentProfile {
|
|
|
296
265
|
for (const userId of Object.keys(dailyUserScores)) {
|
|
297
266
|
const shardIndex = getShardIndex(userId);
|
|
298
267
|
const scores = dailyUserScores[userId];
|
|
299
|
-
|
|
300
268
|
const existingProfiles = existingShards[shardIndex] || {};
|
|
301
269
|
const history = (existingProfiles[userId] || []).slice(); // clone
|
|
302
|
-
|
|
303
270
|
history.push({
|
|
304
271
|
date: dateStr,
|
|
305
272
|
...scores,
|
|
306
273
|
pnl: (pnlScores[userId] || 0)
|
|
307
274
|
});
|
|
308
|
-
|
|
309
275
|
const newHistory = history.slice(-ROLLING_DAYS);
|
|
310
276
|
|
|
311
277
|
// compute rolling averages
|
|
@@ -340,18 +306,8 @@ class UserInvestmentProfile {
|
|
|
340
306
|
};
|
|
341
307
|
}
|
|
342
308
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
*/
|
|
346
|
-
async getResult() {
|
|
347
|
-
return null;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* reset is no longer used by the meta-runner.
|
|
352
|
-
*/
|
|
353
|
-
reset() {
|
|
354
|
-
}
|
|
309
|
+
async getResult() { return null; }
|
|
310
|
+
reset() {}
|
|
355
311
|
}
|
|
356
312
|
|
|
357
313
|
module.exports = UserInvestmentProfile;
|