aiden-shared-calculations-unified 1.0.108 → 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.
Files changed (24) hide show
  1. package/calculations/core/insights-daily-bought-vs-sold-count.js +20 -15
  2. package/calculations/core/insights-daily-ownership-delta.js +12 -10
  3. package/calculations/core/instrument-price-change-1d.js +16 -31
  4. package/calculations/core/instrument-price-momentum-20d.js +16 -26
  5. package/calculations/core/ownership-vs-performance-ytd.js +15 -52
  6. package/calculations/core/ownership-vs-volatility.js +27 -36
  7. package/calculations/core/platform-daily-bought-vs-sold-count.js +28 -26
  8. package/calculations/core/platform-daily-ownership-delta.js +28 -31
  9. package/calculations/core/price-metrics.js +15 -54
  10. package/calculations/core/short-interest-growth.js +6 -13
  11. package/calculations/core/trending-ownership-momentum.js +16 -28
  12. package/calculations/core/user-history-reconstructor.js +48 -49
  13. package/calculations/gauss/cohort-capital-flow.js +34 -71
  14. package/calculations/gauss/cohort-definer.js +61 -142
  15. package/calculations/gem/cohort-momentum-state.js +27 -77
  16. package/calculations/gem/skilled-cohort-flow.js +36 -114
  17. package/calculations/gem/unskilled-cohort-flow.js +36 -112
  18. package/calculations/ghost-book/retail-gamma-exposure.js +14 -61
  19. package/calculations/helix/herd-consensus-score.js +27 -90
  20. package/calculations/helix/winner-loser-flow.js +21 -90
  21. package/calculations/predicative-alpha/cognitive-dissonance.js +25 -91
  22. package/calculations/predicative-alpha/diamond-hand-fracture.js +17 -72
  23. package/calculations/predicative-alpha/mimetic-latency.js +21 -100
  24. package/package.json +1 -1
@@ -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,6 +1,9 @@
1
1
  /**
2
2
  * @fileoverview Reconstructs a user's full trading history timeline from a single snapshot.
3
- * Uses a Scan-Line algorithm to replay history and generate daily stats (Holders, Buys, Sells).
3
+ * Uses a Scan-Line algorithm to replay history and generate daily stats.
4
+ * * FEATURES:
5
+ * 1. Auto-Backfill: If running on the EARLIEST available root data date, it generates the full 365-day history.
6
+ * 2. Incremental Mode: If running on any later date, it only saves that specific date to save I/O.
4
7
  */
5
8
 
6
9
  class UserHistoryReconstructor {
@@ -14,16 +17,16 @@ class UserHistoryReconstructor {
14
17
  type: 'standard',
15
18
  category: 'History Reconstruction',
16
19
  userType: 'all',
17
- // False because we generate history internally from the snapshot
18
20
  isHistorical: false,
19
- rootDataDependencies: ['history']
21
+ rootDataDependencies: ['history'],
22
+ // [NEW] Request system injection for Date Boundaries
23
+ requiresEarliestDataDate: true
20
24
  };
21
25
  }
22
26
 
23
27
  static getDependencies() { return []; }
24
28
 
25
29
  static getSchema() {
26
- // Schema for a single Ticker's stats for a single Day
27
30
  const tickerDailyStats = {
28
31
  "type": "object",
29
32
  "properties": {
@@ -40,16 +43,13 @@ class UserHistoryReconstructor {
40
43
  }
41
44
  };
42
45
 
43
- // Output is now: Date -> Ticker -> Stats
44
46
  return {
45
47
  "type": "object",
46
48
  "patternProperties": {
47
- // Key is YYYY-MM-DD
48
- "^\\d{4}-\\d{2}-\\d{2}$": {
49
+ "^\\d{4}-\\d{2}-\\d{2}$": { // Date Key
49
50
  "type": "object",
50
51
  "patternProperties": {
51
- // Key is Ticker
52
- "^.*$": tickerDailyStats
52
+ "^.*$": tickerDailyStats // Ticker Key
53
53
  }
54
54
  }
55
55
  }
@@ -57,16 +57,31 @@ class UserHistoryReconstructor {
57
57
  }
58
58
 
59
59
  async process(context) {
60
- const { user, math, mappings } = context;
60
+ const { user, math, mappings, date, system } = context;
61
+ const currentExecutionDateStr = date.today;
61
62
 
62
- // 1. Get History (Granular V2 Format)
63
+ // 1. Determine Execution Mode (Backfill vs Incremental)
64
+ // We use the INJECTED value from context.system (provided by StandardExecutor)
65
+ let isEarliestRun = false;
66
+
67
+ if (system && system.earliestHistoryDate) {
68
+ const earliestHistoryStr = system.earliestHistoryDate.toISOString().slice(0, 10);
69
+ if (currentExecutionDateStr <= earliestHistoryStr) {
70
+ isEarliestRun = true;
71
+ }
72
+ } else {
73
+ // Fallback: If system context is missing, default to Incremental to prevent explosion
74
+ // console.warn(`[UserHistoryReconstructor] System context missing. Defaulting to Incremental mode.`);
75
+ }
76
+
77
+ // 2. Get History (Granular V2 Format)
63
78
  const history = math.history.getDailyHistory(user);
64
79
  const allTrades = history?.PublicHistoryPositions || [];
65
80
 
66
81
  if (allTrades.length === 0) return;
67
82
 
68
- // 2. Identify all relevant tickers and dates
69
- const events = []; // { time, type, trade, ticker }
83
+ // 3. Identify all relevant tickers and events
84
+ const events = [];
70
85
  const tickerSet = new Set();
71
86
 
72
87
  for (const trade of allTrades) {
@@ -78,42 +93,34 @@ class UserHistoryReconstructor {
78
93
  const openTime = new Date(trade.OpenDateTime).getTime();
79
94
  const closeTime = trade.CloseDateTime ? new Date(trade.CloseDateTime).getTime() : null;
80
95
 
81
- // Add OPEN event
82
96
  events.push({ time: openTime, type: 'OPEN', trade, ticker });
83
-
84
- // Add CLOSE event (if closed)
85
97
  if (closeTime) { events.push({ time: closeTime, type: 'CLOSE', trade, ticker }); }
86
98
  }
87
99
 
88
- // Sort events by time
89
100
  events.sort((a, b) => a.time - b.time);
90
-
91
101
  if (events.length === 0) return;
92
102
 
93
- // 3. Scan-Line Execution (Replay History)
94
- const userTimeline = {}; // { "2024-01-01": { "AAPL": {...} } }
103
+ // 4. Scan-Line Execution (Replay History)
104
+ const userTimeline = {};
105
+ const activePositions = new Map();
95
106
 
96
- // Tracking State
97
- const activePositions = new Map(); // Map<PositionID, Trade>
98
-
99
- // Determine Start and End dates for the loop
100
107
  const firstEventTime = events[0].time;
101
108
  const lastEventTime = events[events.length - 1].time;
102
109
 
103
110
  const startDate = new Date(firstEventTime); startDate.setUTCHours(0,0,0,0);
104
- const endDate = new Date(lastEventTime); endDate.setUTCHours(0,0,0,0);
111
+ const endDate = new Date(lastEventTime); endDate.setUTCHours(0,0,0,0);
105
112
 
106
113
  let currentEventIdx = 0;
107
114
  const oneDayMs = 86400000;
108
115
 
109
- // Iterate day by day from first trade to last trade
116
+ // Iterate day by day
110
117
  for (let d = startDate.getTime(); d <= endDate.getTime(); d += oneDayMs) {
111
118
  const dateStr = new Date(d).toISOString().slice(0, 10);
112
119
  const dayEnd = d + oneDayMs - 1;
113
120
 
114
- const dayStats = {}; // Ticker -> Stats
121
+ const dayStats = {};
115
122
 
116
- // Process all events that happened TODAY (before EOD)
123
+ // Process events for TODAY
117
124
  while (currentEventIdx < events.length && events[currentEventIdx].time <= dayEnd) {
118
125
  const event = events[currentEventIdx];
119
126
  const { ticker, trade, type } = event;
@@ -135,7 +142,7 @@ class UserHistoryReconstructor {
135
142
  currentEventIdx++;
136
143
  }
137
144
 
138
- // Snapshot the "Held" State at EOD
145
+ // Snapshot "Held" State at EOD
139
146
  for (const [posId, trade] of activePositions) {
140
147
  const ticker = mappings.instrumentToTicker[trade.InstrumentID];
141
148
  if (!ticker) continue;
@@ -143,16 +150,15 @@ class UserHistoryReconstructor {
143
150
  if (!dayStats[ticker]) this.initTickerStats(dayStats, ticker);
144
151
 
145
152
  const stats = dayStats[ticker];
146
- stats.isHolder = 1; // User is a holder today
153
+ stats.isHolder = 1;
147
154
  stats.holdCount++;
148
155
  stats.sumEntry += (trade.OpenRate || 0);
149
156
  stats.sumLev += (trade.Leverage || 1);
150
157
  }
151
158
 
152
- // Finalize Averages for this Day
159
+ // Finalize Averages
153
160
  for (const ticker in dayStats) {
154
161
  const stats = dayStats[ticker];
155
-
156
162
  if (stats.holdCount > 0) {
157
163
  stats.avgEntry = stats.sumEntry / stats.holdCount;
158
164
  stats.avgLeverage = stats.sumLev / stats.holdCount;
@@ -160,34 +166,27 @@ class UserHistoryReconstructor {
160
166
  if (stats.didBuy > 0) {
161
167
  stats.buyLeverage = stats.sumBuyLev / stats.didBuy;
162
168
  }
163
-
164
- // Cleanup temp sums
165
- delete stats.sumEntry;
166
- delete stats.sumLev;
167
- delete stats.sumBuyLev;
168
- delete stats.holdCount;
169
+ delete stats.sumEntry; delete stats.sumLev; delete stats.sumBuyLev; delete stats.holdCount;
169
170
  }
170
171
 
171
- // Store in timeline if any activity exists for this day
172
+ // [LOGIC UPDATE] Selective Storage
173
+ // 1. If this is the EARLIEST run (Backfill), we save ALL calculated days.
174
+ // 2. If this is a normal run, we ONLY save the stats for the execution date.
172
175
  if (Object.keys(dayStats).length > 0) {
173
- userTimeline[dateStr] = dayStats;
176
+ if (isEarliestRun) {
177
+ userTimeline[dateStr] = dayStats;
178
+ } else if (dateStr === currentExecutionDateStr) {
179
+ userTimeline[dateStr] = dayStats;
180
+ }
174
181
  }
175
182
  }
176
183
 
177
- // 4. Output the full timeline for this user
178
- // The ResultCommitter will handle splitting this into date buckets.
179
184
  this.results[user.id] = userTimeline;
180
185
  }
181
186
 
182
187
  initTickerStats(dayStats, ticker) {
183
188
  dayStats[ticker] = {
184
- isHolder: 0,
185
- didBuy: 0,
186
- didSell: 0,
187
- sumEntry: 0,
188
- sumLev: 0,
189
- holdCount: 0,
190
- sumBuyLev: 0,
189
+ isHolder: 0, didBuy: 0, didSell: 0, sumEntry: 0, sumLev: 0, holdCount: 0, sumBuyLev: 0,
191
190
  closeReasons: { "0": 0, "1": 0, "5": 0 }
192
191
  };
193
192
  }