aiden-shared-calculations-unified 1.0.109 → 1.0.110

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,10 +1,6 @@
1
- /**
2
- * @fileoverview Calculation (Pass 1) for ownership delta (users added/lost per asset).
3
- * REFACTORED: Tracks net change in number of owners. ---> TODO IN TEST ENVIRONMENT THE VALUE OF NUMBER OF CLOSURES ALWAYS RETURNS 0, IS THIS A COMPUTATION BUG OR A TEST HARNESS BUG?
4
- */
5
1
  class PlatformDailyOwnershipDelta {
6
2
  constructor() {
7
- this.assetChanges = new Map(); // { instId: { added: 0, removed: 0 } }
3
+ this.assetChanges = new Map();
8
4
  this.tickerMap = null;
9
5
  }
10
6
 
@@ -26,16 +22,17 @@ class PlatformDailyOwnershipDelta {
26
22
  "properties": {
27
23
  "owners_added": { "type": "number" },
28
24
  "owners_removed": { "type": "number" },
29
- "net_ownership_change": { "type": "number" }
25
+ "net_ownership_change": { "type": "number" },
26
+ "total_owners_today": { "type": "number" }
30
27
  },
31
- "required": ["owners_added", "owners_removed", "net_ownership_change"]
28
+ "required": ["owners_added", "owners_removed", "net_ownership_change", "total_owners_today"]
32
29
  };
33
30
  return { "type": "object", "patternProperties": { "^.*$": tickerSchema } };
34
31
  }
35
32
 
36
33
  _initAsset(instId) {
37
34
  if (!this.assetChanges.has(instId)) {
38
- this.assetChanges.set(instId, { added: 0, removed: 0 });
35
+ this.assetChanges.set(instId, { added: 0, removed: 0, total: 0 });
39
36
  }
40
37
  }
41
38
 
@@ -53,28 +50,29 @@ class PlatformDailyOwnershipDelta {
53
50
  const { mappings, user } = context;
54
51
  if (!this.tickerMap) this.tickerMap = mappings.instrumentToTicker;
55
52
 
56
- // If either portfolio is missing, we can't calculate delta for this user
57
- if (!user.portfolio.today || !user.portfolio.yesterday) return;
58
-
59
53
  const todayPositions = extract.getPositions(user.portfolio.today, user.type);
60
- const yesterdayPositions = extract.getPositions(user.portfolio.yesterday, user.type);
61
-
62
54
  const tSet = this._getOwnedInstruments(todayPositions, extract);
63
- const ySet = this._getOwnedInstruments(yesterdayPositions, extract);
64
55
 
65
- // Added: In Today, Not Yesterday
56
+ // Establish current total for Baseline
66
57
  for (const instId of tSet) {
67
- if (!ySet.has(instId)) {
68
- this._initAsset(instId);
69
- this.assetChanges.get(instId).added++;
70
- }
58
+ this._initAsset(instId);
59
+ this.assetChanges.get(instId).total++;
71
60
  }
72
61
 
73
- // Removed: In Yesterday, Not Today
74
- for (const instId of ySet) {
75
- if (!tSet.has(instId)) {
76
- this._initAsset(instId);
77
- this.assetChanges.get(instId).removed++;
62
+ if (user.portfolio.yesterday) {
63
+ const yesterdayPositions = extract.getPositions(user.portfolio.yesterday, user.type);
64
+ const ySet = this._getOwnedInstruments(yesterdayPositions, extract);
65
+ for (const instId of tSet) {
66
+ if (!ySet.has(instId)) {
67
+ this._initAsset(instId);
68
+ this.assetChanges.get(instId).added++;
69
+ }
70
+ }
71
+ for (const instId of ySet) {
72
+ if (!tSet.has(instId)) {
73
+ this._initAsset(instId);
74
+ this.assetChanges.get(instId).removed++;
75
+ }
78
76
  }
79
77
  }
80
78
  }
@@ -84,13 +82,12 @@ class PlatformDailyOwnershipDelta {
84
82
  const result = {};
85
83
  for (const [instId, data] of this.assetChanges.entries()) {
86
84
  const ticker = this.tickerMap[instId] || `id_${instId}`;
87
- if (data.added > 0 || data.removed > 0) {
88
- result[ticker] = {
89
- owners_added: data.added,
90
- owners_removed: data.removed,
91
- net_ownership_change: data.added - data.removed
92
- };
93
- }
85
+ result[ticker] = {
86
+ owners_added: data.added,
87
+ owners_removed: data.removed,
88
+ net_ownership_change: data.added - data.removed,
89
+ total_owners_today: data.total
90
+ };
94
91
  }
95
92
  return result;
96
93
  }
@@ -1,15 +1,9 @@
1
- /**
2
- * @fileoverview Calculation (Pass 1 - Meta) for historical price metrics.
3
- * FIXED: Supports Sharded/Batched Execution.
4
- * MOVED: Sector aggregation logic moved to getResult() to handle sharding correctly.
5
- */
6
1
  const RANGES = [7, 30, 90, 365];
7
2
  const TRADING_DAYS_PER_YEAR = 252;
8
3
 
9
4
  class CorePriceMetrics {
10
5
  constructor() {
11
6
  this.result = { by_instrument: {}, by_sector: {} };
12
- // We persist mappings here because we process shard-by-shard
13
7
  this.mappings = null;
14
8
  }
15
9
 
@@ -49,13 +43,11 @@ class CorePriceMetrics {
49
43
  const prices = [];
50
44
  let currentDate = new Date(endDateStr + 'T00:00:00Z');
51
45
  let lastPrice = null;
52
-
53
46
  for (let i = 0; i < numDays; i++) {
54
47
  const targetDateStr = currentDate.toISOString().slice(0, 10);
55
48
  let price = this._findPriceOnOrBefore(priceHistoryObj, targetDateStr);
56
49
  if (price === null) price = lastPrice;
57
50
  else lastPrice = price;
58
-
59
51
  if (price !== null) prices.push(price);
60
52
  currentDate.setUTCDate(currentDate.getUTCDate() - 1);
61
53
  }
@@ -63,9 +55,9 @@ class CorePriceMetrics {
63
55
  }
64
56
 
65
57
  _calculateStats(priceArray) {
66
- if (priceArray.length < 2) return { stdDev: 0, mean: 0, drawdown: 0 };
58
+ const currentPrice = priceArray[priceArray.length - 1] || null;
59
+ if (priceArray.length < 2) return { stdDev: null, mean: null, drawdown: null, currentPrice };
67
60
 
68
- // Drawdown
69
61
  let maxDrawdown = 0, peak = -Infinity;
70
62
  for (const price of priceArray) {
71
63
  if (price > peak) peak = price;
@@ -74,8 +66,6 @@ class CorePriceMetrics {
74
66
  if (dd < maxDrawdown) maxDrawdown = dd;
75
67
  }
76
68
  }
77
-
78
- // Returns & StdDev
79
69
  const returns = [];
80
70
  for (let i = 1; i < priceArray.length; i++) {
81
71
  const prev = priceArray[i - 1];
@@ -84,78 +74,53 @@ class CorePriceMetrics {
84
74
  }
85
75
  const mean = returns.reduce((a, b) => a + b, 0) / returns.length;
86
76
  const variance = returns.reduce((a, b) => a + (b - mean) ** 2, 0) / (returns.length - 1);
87
-
88
- return { stdDev: Math.sqrt(variance), mean, drawdown: maxDrawdown };
77
+ return { stdDev: Math.sqrt(variance), mean, drawdown: maxDrawdown, currentPrice };
89
78
  }
90
79
 
91
80
  process(context) {
92
81
  const { mappings, prices, date } = context;
93
82
  const { instrumentToTicker } = mappings;
94
-
95
- // Save mappings for the final aggregation step
96
83
  if (!this.mappings) this.mappings = mappings;
97
-
98
84
  const priceData = prices?.history;
99
85
  const todayDateStr = date.today;
86
+ if (!priceData || !todayDateStr) return;
100
87
 
101
- if (!priceData || !todayDateStr) {
102
- return;
103
- }
104
-
105
- // Iterate over THIS SHARD'S data
106
88
  for (const p of Object.values(priceData)) {
107
89
  const ticker = instrumentToTicker[p.instrumentId];
108
90
  if (!ticker) continue;
109
-
110
- const metrics = {};
91
+ const metrics = { baseline_price: null };
111
92
  for (const range of RANGES) {
112
93
  const priceArray = this._getHistoricalPriceArray(p, todayDateStr, range + 1);
113
94
  const stats = this._calculateStats(priceArray);
114
-
115
- metrics[`stdev_${range}d`] = stats.stdDev;
116
- metrics[`max_drawdown_${range}d`] = stats.drawdown;
117
- metrics[`volatility_annualized_${range}d`] = stats.stdDev * Math.sqrt(TRADING_DAYS_PER_YEAR);
118
- metrics[`sharpe_ratio_${range}d`] = stats.stdDev > 0
119
- ? (stats.mean / stats.stdDev) * Math.sqrt(TRADING_DAYS_PER_YEAR)
120
- : 0;
95
+ metrics.baseline_price = stats.currentPrice;
96
+ metrics[`stdev_${range}d`] = (stats.stdDev !== null && isFinite(stats.stdDev)) ? stats.stdDev : null;
97
+ metrics[`max_drawdown_${range}d`] = (stats.drawdown !== null && isFinite(stats.drawdown)) ? stats.drawdown : null;
98
+ metrics[`volatility_annualized_${range}d`] = (stats.stdDev !== null) ? stats.stdDev * Math.sqrt(TRADING_DAYS_PER_YEAR) : null;
99
+ metrics[`sharpe_ratio_${range}d`] = (stats.stdDev > 0) ? (stats.mean / stats.stdDev) * Math.sqrt(TRADING_DAYS_PER_YEAR) : null;
121
100
  }
122
- // Accumulate into by_instrument
123
101
  this.result.by_instrument[ticker] = metrics;
124
102
  }
125
103
  }
126
104
 
127
105
  async getResult() {
128
- // Perform Sector Aggregation HERE (after all shards are processed)
129
106
  const by_instrument = this.result.by_instrument;
130
107
  const instrumentToSector = this.mappings?.instrumentToSector || {};
131
108
  const instrumentToTicker = this.mappings?.instrumentToTicker || {};
132
-
133
- // Reverse map ticker -> instrumentId
134
109
  const tickerToInstrument = {};
135
- for(const [id, tick] of Object.entries(instrumentToTicker)) {
136
- tickerToInstrument[tick] = id;
137
- }
110
+ for(const [id, tick] of Object.entries(instrumentToTicker)) tickerToInstrument[tick] = id;
138
111
 
139
112
  const sectorAggs = {};
140
113
  for (const ticker in by_instrument) {
141
114
  const instId = tickerToInstrument[ticker];
142
115
  const sector = instrumentToSector[instId] || "Unknown";
143
-
144
116
  if (!sectorAggs[sector]) sectorAggs[sector] = { metrics: {}, counts: {} };
145
-
146
117
  const data = by_instrument[ticker];
147
118
  for (const key in data) {
148
- if (!sectorAggs[sector].metrics[key]) {
149
- sectorAggs[sector].metrics[key] = 0;
150
- sectorAggs[sector].counts[key] = 0;
151
- }
152
- if (data[key] !== null) {
153
- sectorAggs[sector].metrics[key] += data[key];
154
- sectorAggs[sector].counts[key]++;
155
- }
119
+ if (key === 'baseline_price') continue;
120
+ if (!sectorAggs[sector].metrics[key]) { sectorAggs[sector].metrics[key] = 0; sectorAggs[sector].counts[key] = 0; }
121
+ if (data[key] !== null) { sectorAggs[sector].metrics[key] += data[key]; sectorAggs[sector].counts[key]++; }
156
122
  }
157
123
  }
158
-
159
124
  const by_sector = {};
160
125
  for (const sector in sectorAggs) {
161
126
  by_sector[sector] = {};
@@ -164,14 +129,10 @@ class CorePriceMetrics {
164
129
  by_sector[sector][`average_${key}`] = count > 0 ? sectorAggs[sector].metrics[key] / count : null;
165
130
  }
166
131
  }
167
-
168
132
  this.result.by_sector = by_sector;
169
133
  return this.result;
170
134
  }
171
135
 
172
- reset() {
173
- this.result = { by_instrument: {}, by_sector: {} };
174
- this.mappings = null;
175
- }
136
+ reset() { this.result = { by_instrument: {}, by_sector: {} }; this.mappings = null; }
176
137
  }
177
138
  module.exports = CorePriceMetrics;
@@ -19,7 +19,7 @@ class ShortInterestGrowth {
19
19
  "TICKER": {
20
20
  "shortCount": 0,
21
21
  "shortSparkline": [0, 0, 0, 0, 0, 0, 0],
22
- "growth7d": 0.0
22
+ "growth7d": { "type": ["number", "null"] }
23
23
  }
24
24
  };
25
25
  }
@@ -27,7 +27,6 @@ class ShortInterestGrowth {
27
27
  async process(context) {
28
28
  const { insights: insightsHelper } = context.math;
29
29
  const { previousComputed, mappings } = context;
30
-
31
30
  const dailyInsights = insightsHelper.getInsights(context, 'today');
32
31
  const previousState = previousComputed['short-interest-growth'] || {};
33
32
 
@@ -36,34 +35,28 @@ class ShortInterestGrowth {
36
35
  const ticker = mappings.instrumentToTicker[instId];
37
36
  if (!ticker) continue;
38
37
 
39
- // Calculate Short Count: Total Owners * (Sell % / 100)
40
38
  const shortCount = insightsHelper.getShortCount(insight);
41
-
42
39
  const prevState = previousState[ticker] || { shortSparkline: [] };
43
40
  const sparkline = [...(prevState.shortSparkline || [])];
44
-
45
41
  sparkline.push(shortCount);
46
42
  if (sparkline.length > 7) sparkline.shift();
47
43
 
48
- // Calculate simple 7-day growth %
49
- let growth7d = 0;
50
- if (sparkline.length > 1) {
44
+ let growth7d = null;
45
+ if (sparkline.length >= 7) {
51
46
  const start = sparkline[0];
52
47
  const end = sparkline[sparkline.length - 1];
53
- if (start > 0) {
54
- growth7d = ((end - start) / start) * 100;
55
- }
48
+ if (start > 0) growth7d = ((end - start) / start) * 100;
56
49
  }
57
50
 
58
51
  this.results[ticker] = {
59
52
  shortCount,
60
53
  shortSparkline: sparkline,
61
- growth7d
54
+ growth7d: (growth7d !== null && isFinite(growth7d)) ? growth7d : null
62
55
  };
63
56
  }
64
57
  }
65
58
 
66
59
  async getResult() { return this.results; }
60
+ reset() { this.results = {}; }
67
61
  }
68
-
69
62
  module.exports = ShortInterestGrowth;
@@ -7,7 +7,7 @@ class TrendingOwnershipMomentum {
7
7
  type: 'meta',
8
8
  category: 'core',
9
9
  userType: 'n/a',
10
- isHistorical: true, // Needed for rolling history
10
+ isHistorical: true,
11
11
  rootDataDependencies: ['insights']
12
12
  };
13
13
  }
@@ -18,9 +18,9 @@ class TrendingOwnershipMomentum {
18
18
  return {
19
19
  "TICKER": {
20
20
  "currentOwners": 0,
21
- "sparkline": [0, 0, 0, 0, 0, 0, 0], // 7-Day History
22
- "momentumScore": 0.0, // Slope of the trend
23
- "trend": "string" // 'RISING', 'FALLING', 'STABLE'
21
+ "sparkline": [0, 0, 0, 0, 0, 0, 0],
22
+ "momentumScore": { "type": ["number", "null"] },
23
+ "trend": "string"
24
24
  }
25
25
  };
26
26
  }
@@ -28,12 +28,8 @@ class TrendingOwnershipMomentum {
28
28
  async process(context) {
29
29
  const { insights: insightsHelper } = context.math;
30
30
  const { previousComputed, mappings } = context;
31
-
32
- // 1. Get Today's Data
33
31
  const dailyInsights = insightsHelper.getInsights(context, 'today');
34
- if (!dailyInsights.length) return;
35
-
36
- // 2. Get Yesterday's State (for rolling history)
32
+ if (!dailyInsights || !dailyInsights.length) return;
37
33
  const previousState = previousComputed['trending-ownership-momentum'] || {};
38
34
 
39
35
  for (const insight of dailyInsights) {
@@ -43,45 +39,37 @@ class TrendingOwnershipMomentum {
43
39
 
44
40
  const currentCount = insightsHelper.getTotalOwners(insight);
45
41
  const prevState = previousState[ticker] || { sparkline: [] };
46
-
47
- // 3. Update Rolling Window (Last 7 Days)
48
- // Create new array copy to avoid mutation issues
49
42
  const sparkline = [...(prevState.sparkline || [])];
50
43
  sparkline.push(currentCount);
51
-
52
- // Keep only last 7 entries
53
44
  if (sparkline.length > 7) sparkline.shift();
54
45
 
55
- // 4. Calculate Momentum Score (Linear Regression Slope)
56
- let momentumScore = 0;
57
- if (sparkline.length >= 2) {
46
+ let momentumScore = null;
47
+ let trend = 'WARM_UP';
48
+
49
+ if (sparkline.length >= 7) {
58
50
  const n = sparkline.length;
59
51
  let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0;
60
52
  for (let i = 0; i < n; i++) {
61
- sumX += i;
62
- sumY += sparkline[i];
63
- sumXY += i * sparkline[i];
64
- sumXX += i * i;
53
+ sumX += i; sumY += sparkline[i]; sumXY += i * sparkline[i]; sumXX += i * i;
65
54
  }
66
55
  const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
67
- // Normalize slope by current count to get percentage-like momentum
68
56
  momentumScore = currentCount > 0 ? (slope / currentCount) * 100 : 0;
57
+
58
+ trend = 'STABLE';
59
+ if (momentumScore > 0.5) trend = 'RISING';
60
+ if (momentumScore < -0.5) trend = 'FALLING';
69
61
  }
70
62
 
71
- let trend = 'STABLE';
72
- if (momentumScore > 0.5) trend = 'RISING';
73
- if (momentumScore < -0.5) trend = 'FALLING';
74
-
75
63
  this.results[ticker] = {
76
64
  currentOwners: currentCount,
77
65
  sparkline,
78
- momentumScore,
66
+ momentumScore: (momentumScore !== null && isFinite(momentumScore)) ? momentumScore : null,
79
67
  trend
80
68
  };
81
69
  }
82
70
  }
83
71
 
84
72
  async getResult() { return this.results; }
73
+ reset() { this.results = {}; }
85
74
  }
86
-
87
75
  module.exports = TrendingOwnershipMomentum;
@@ -1,12 +1,8 @@
1
- /**
2
- * @fileoverview GAUSS Product Line (Pass 3)
3
- * REFACTORED: Implemented getResult logic and mapping caching.
4
- */
5
1
  class CohortCapitalFlow {
6
2
  constructor() {
7
3
  this.cohortFlows = new Map();
8
4
  this.cohortMap = new Map();
9
- this.tickerMap = null; // Cache mappings
5
+ this.tickerMap = null;
10
6
  this.dependenciesLoaded = false;
11
7
  }
12
8
 
@@ -14,36 +10,26 @@ class CohortCapitalFlow {
14
10
  const flowSchema = {
15
11
  "type": "object",
16
12
  "properties": {
17
- "net_flow_percentage": { "type": "number" },
18
- "net_flow_contribution": { "type": "number" }
13
+ "net_flow_percentage": { "type": ["number", "null"] },
14
+ "net_flow_contribution": { "type": ["number", "null"] },
15
+ "total_invested_today": { "type": "number" } // Baseline
19
16
  },
20
- "required": ["net_flow_percentage", "net_flow_contribution"]
21
- };
22
- return {
23
- "type": "object",
24
- "patternProperties": { "^.*$": { "type": "object", "patternProperties": { "^.*$": flowSchema } } }
17
+ "required": ["net_flow_percentage", "net_flow_contribution", "total_invested_today"]
25
18
  };
19
+ return { "type": "object", "patternProperties": { "^.*$": { "type": "object", "patternProperties": { "^.*$": flowSchema } } } };
26
20
  }
27
21
 
28
22
  static getMetadata() {
29
- return {
30
- type: 'standard',
31
- rootDataDependencies: ['portfolio', 'history'],
32
- isHistorical: true,
33
- userType: 'all',
34
- category: 'gauss'
35
- };
23
+ return { type: 'standard', rootDataDependencies: ['portfolio', 'history'], isHistorical: true, userType: 'all', category: 'gauss' };
36
24
  }
37
25
 
38
- static getDependencies() {
39
- return ['cohort-definer', 'instrument-price-change-1d'];
40
- }
26
+ static getDependencies() { return ['cohort-definer', 'instrument-price-change-1d']; }
41
27
 
42
28
  _initFlowData(cohortName, instrumentId) {
43
29
  if (!this.cohortFlows.has(cohortName)) this.cohortFlows.set(cohortName, new Map());
44
30
  if (!this.cohortFlows.get(cohortName).has(instrumentId)) {
45
31
  this.cohortFlows.get(cohortName).set(instrumentId, {
46
- total_invested_yesterday: 0, total_invested_today: 0, price_change_yesterday: 0
32
+ total_invested_yesterday: 0, total_invested_today: 0, price_change_yesterday: 0, has_yesterday: false
47
33
  });
48
34
  }
49
35
  }
@@ -62,84 +48,61 @@ class CohortCapitalFlow {
62
48
  process(context) {
63
49
  const { user, computed, mappings, math } = context;
64
50
  const { extract } = math;
65
-
66
- // Cache mapping for getResult phase
67
51
  if (!this.tickerMap) this.tickerMap = mappings.instrumentToTicker;
68
-
69
52
  this._loadDependencies(computed);
70
-
71
53
  const cohortName = this.cohortMap.get(user.id);
72
54
  if (!cohortName) return;
73
55
 
74
56
  const priceChangeMap = computed['instrument-price-change-1d'];
75
- if (!priceChangeMap) return;
76
-
77
- const yPos = extract.getPositions(user.portfolio.yesterday, user.type);
78
57
  const tPos = extract.getPositions(user.portfolio.today, user.type);
58
+ const yPos = user.portfolio.yesterday ? extract.getPositions(user.portfolio.yesterday, user.type) : [];
79
59
 
80
60
  const yPosMap = new Map(yPos.map(p => [extract.getInstrumentId(p), p]));
81
61
  const tPosMap = new Map(tPos.map(p => [extract.getInstrumentId(p), p]));
82
62
  const allInstrumentIds = new Set([...yPosMap.keys(), ...tPosMap.keys()]);
83
63
 
84
64
  for (const instId of allInstrumentIds) {
85
- if (!instId) continue;
86
-
87
65
  this._initFlowData(cohortName, instId);
88
66
  const asset = this.cohortFlows.get(cohortName).get(instId);
89
-
90
- const yInvested = extract.getPositionWeight(yPosMap.get(instId), user.type);
91
67
  const tInvested = extract.getPositionWeight(tPosMap.get(instId), user.type);
92
-
93
- if (yInvested > 0) {
94
- asset.total_invested_yesterday += yInvested;
95
-
96
- const ticker = this.tickerMap[instId];
97
- const yPriceChange_pct = (ticker && priceChangeMap[ticker]) ? priceChangeMap[ticker].change_1d_pct : 0;
98
-
99
- asset.price_change_yesterday += (yPriceChange_pct / 100.0) * yInvested;
100
- }
101
- if (tInvested > 0) {
102
- asset.total_invested_today += tInvested;
68
+ if (tInvested > 0) asset.total_invested_today += tInvested;
69
+
70
+ if (user.portfolio.yesterday) {
71
+ asset.has_yesterday = true;
72
+ const yInvested = extract.getPositionWeight(yPosMap.get(instId), user.type);
73
+ if (yInvested > 0) {
74
+ asset.total_invested_yesterday += yInvested;
75
+ const ticker = this.tickerMap[instId];
76
+ const yPriceChange = (ticker && priceChangeMap?.[ticker]) ? priceChangeMap[ticker].change_1d_pct : 0;
77
+ asset.price_change_yesterday += ((yPriceChange || 0) / 100.0) * yInvested;
78
+ }
103
79
  }
104
80
  }
105
81
  }
106
82
 
107
83
  async getResult() {
108
84
  const finalResult = {};
109
-
110
- if (!this.tickerMap) return {}; // Should be populated by process()
111
-
85
+ if (!this.tickerMap) return {};
112
86
  for (const [cohortName, assetsMap] of this.cohortFlows.entries()) {
113
87
  finalResult[cohortName] = {};
114
-
115
88
  for (const [instId, data] of assetsMap.entries()) {
116
89
  const ticker = this.tickerMap[instId] || `id_${instId}`;
117
-
118
- // Logic: Flow = Today - (Yesterday * (1 + PriceChange))
119
- // This isolates the capital movement from the price action.
120
- const expectedToday = data.total_invested_yesterday + data.price_change_yesterday;
121
- const netFlow = data.total_invested_today - expectedToday;
122
-
123
- // Avoid divide by zero or noise for very small numbers
124
- const denominator = Math.max(1, (data.total_invested_yesterday + data.total_invested_today) / 2);
125
- const netFlowPct = (netFlow / denominator) * 100;
126
-
90
+ let netFlow = null, netFlowPct = null;
91
+ if (data.has_yesterday) {
92
+ const expectedToday = data.total_invested_yesterday + data.price_change_yesterday;
93
+ netFlow = data.total_invested_today - expectedToday;
94
+ const denom = Math.max(1, (data.total_invested_yesterday + data.total_invested_today) / 2);
95
+ netFlowPct = (netFlow / denom) * 100;
96
+ }
127
97
  finalResult[cohortName][ticker] = {
128
- net_flow_percentage: netFlowPct,
129
- net_flow_contribution: netFlow
98
+ net_flow_percentage: (netFlowPct !== null && isFinite(netFlowPct)) ? netFlowPct : null,
99
+ net_flow_contribution: (netFlow !== null && isFinite(netFlow)) ? netFlow : null,
100
+ total_invested_today: data.total_invested_today
130
101
  };
131
102
  }
132
103
  }
133
-
134
104
  return finalResult;
135
105
  }
136
-
137
- reset() {
138
- this.cohortFlows.clear();
139
- this.cohortMap.clear();
140
- this.tickerMap = null;
141
- this.dependenciesLoaded = false;
142
- }
106
+ reset() { this.cohortFlows.clear(); this.cohortMap.clear(); this.tickerMap = null; this.dependenciesLoaded = false; }
143
107
  }
144
-
145
- module.exports = CohortCapitalFlow;
108
+ module.exports = CohortCapitalFlow;