aiden-shared-calculations-unified 1.0.107 → 1.0.109

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.
@@ -4,7 +4,9 @@
4
4
  */
5
5
  class AssetPnlStatus {
6
6
  constructor() {
7
- this.userStats = new Map();
7
+ // [FIX 1] Use 'this.results' directly.
8
+ // The StandardExecutor monitors this property to perform Batch Flushing.
9
+ this.results = {};
8
10
  }
9
11
 
10
12
  static getMetadata() {
@@ -22,29 +24,26 @@ class AssetPnlStatus {
22
24
  static getSchema() {
23
25
  return {
24
26
  "type": "object",
25
- "properties": {
26
- "by_user": {
27
+ // [FIX 2] Flattened Schema (removed "by_user" wrapper)
28
+ "patternProperties": {
29
+ "^[0-9]+$": {
27
30
  "type": "object",
28
- "patternProperties": {
29
- "^[0-9]+$": {
30
- "type": "object",
31
- "properties": {
32
- "winners": { "type": "array", "items": { "type": "string" } },
33
- "losers": { "type": "array", "items": { "type": "string" } }
34
- },
35
- "required": ["winners", "losers"]
36
- }
37
- }
31
+ "properties": {
32
+ "winners": { "type": "array", "items": { "type": "string" } },
33
+ "losers": { "type": "array", "items": { "type": "string" } }
34
+ },
35
+ "required": ["winners", "losers"]
38
36
  }
39
37
  }
40
38
  };
41
39
  }
42
40
 
43
41
  _initUser(userId) {
44
- if (!this.userStats.has(userId)) {
45
- this.userStats.set(userId, { winners: new Set(), losers: new Set() });
42
+ // [FIX 3] Direct access to this.results
43
+ if (!this.results[userId]) {
44
+ this.results[userId] = { winners: [], losers: [] };
46
45
  }
47
- return this.userStats.get(userId);
46
+ return this.results[userId];
48
47
  }
49
48
 
50
49
  process(context) {
@@ -55,7 +54,9 @@ class AssetPnlStatus {
55
54
  const positions = extract.getPositions(user.portfolio.today, user.type);
56
55
  if (!positions || positions.length === 0) return;
57
56
 
58
- let stats = null;
57
+ // Use temporary sets for processing to keep logic clean, then convert to array for storage
58
+ const winners = new Set();
59
+ const losers = new Set();
59
60
 
60
61
  for (const pos of positions) {
61
62
  const instId = extract.getInstrumentId(pos);
@@ -68,35 +69,26 @@ class AssetPnlStatus {
68
69
  const sector = mappings.instrumentToSector[instId];
69
70
  const isWinner = pnl > 0;
70
71
 
71
- if (ticker || sector) {
72
- if (!stats) stats = this._initUser(userId);
73
- if (ticker) {
74
- if (isWinner) stats.winners.add(ticker); else stats.losers.add(ticker);
75
- }
76
- if (sector) {
77
- if (isWinner) stats.winners.add(sector); else stats.losers.add(sector);
78
- }
79
- }
72
+ if (ticker) { isWinner ? winners.add(ticker) : losers.add(ticker); }
73
+ if (sector) { isWinner ? winners.add(sector) : losers.add(sector); }
80
74
  }
81
- }
82
75
 
83
- async getResult() {
84
- const byUser = {};
85
-
86
- for (const [userId, stats] of this.userStats) {
87
- if (stats.winners.size > 0 || stats.losers.size > 0) {
88
- byUser[userId] = {
89
- winners: Array.from(stats.winners),
90
- losers: Array.from(stats.losers)
91
- };
92
- }
76
+ if (winners.size > 0 || losers.size > 0) {
77
+ this.results[userId] = {
78
+ winners: Array.from(winners),
79
+ losers: Array.from(losers)
80
+ };
93
81
  }
82
+ }
94
83
 
95
- // Return flat object. Orchestrator handles 1MB limit.
96
- return { by_user: byUser };
84
+ async getResult() {
85
+ // [FIX 4] Return 'this.results' directly.
86
+ // This is a FLAT object { "user1": {...}, "user2": {...} }
87
+ // The ResultCommitter can now shard this into 50-user chunks if needed.
88
+ return this.results;
97
89
  }
98
90
 
99
- reset() { this.userStats.clear(); }
91
+ // [FIX 5] No manual reset needed, StandardExecutor does it.
100
92
  }
101
93
 
102
94
  module.exports = AssetPnlStatus;
@@ -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,44 +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
- if (closeTime) {
86
- events.push({ time: closeTime, type: 'CLOSE', trade, ticker });
87
- }
97
+ if (closeTime) { events.push({ time: closeTime, type: 'CLOSE', trade, ticker }); }
88
98
  }
89
99
 
90
- // Sort events by time
91
100
  events.sort((a, b) => a.time - b.time);
92
-
93
101
  if (events.length === 0) return;
94
102
 
95
- // 3. Scan-Line Execution (Replay History)
96
- const userTimeline = {}; // { "2024-01-01": { "AAPL": {...} } }
97
-
98
- // Tracking State
99
- const activePositions = new Map(); // Map<PositionID, Trade>
103
+ // 4. Scan-Line Execution (Replay History)
104
+ const userTimeline = {};
105
+ const activePositions = new Map();
100
106
 
101
- // Determine Start and End dates for the loop
102
107
  const firstEventTime = events[0].time;
103
108
  const lastEventTime = events[events.length - 1].time;
104
109
 
105
110
  const startDate = new Date(firstEventTime); startDate.setUTCHours(0,0,0,0);
106
- const endDate = new Date(lastEventTime); endDate.setUTCHours(0,0,0,0);
111
+ const endDate = new Date(lastEventTime); endDate.setUTCHours(0,0,0,0);
107
112
 
108
113
  let currentEventIdx = 0;
109
114
  const oneDayMs = 86400000;
110
115
 
111
- // Iterate day by day from first trade to last trade
116
+ // Iterate day by day
112
117
  for (let d = startDate.getTime(); d <= endDate.getTime(); d += oneDayMs) {
113
118
  const dateStr = new Date(d).toISOString().slice(0, 10);
114
119
  const dayEnd = d + oneDayMs - 1;
115
120
 
116
- const dayStats = {}; // Ticker -> Stats
121
+ const dayStats = {};
117
122
 
118
- // Process all events that happened TODAY (before EOD)
123
+ // Process events for TODAY
119
124
  while (currentEventIdx < events.length && events[currentEventIdx].time <= dayEnd) {
120
125
  const event = events[currentEventIdx];
121
126
  const { ticker, trade, type } = event;
@@ -137,7 +142,7 @@ class UserHistoryReconstructor {
137
142
  currentEventIdx++;
138
143
  }
139
144
 
140
- // Snapshot the "Held" State at EOD
145
+ // Snapshot "Held" State at EOD
141
146
  for (const [posId, trade] of activePositions) {
142
147
  const ticker = mappings.instrumentToTicker[trade.InstrumentID];
143
148
  if (!ticker) continue;
@@ -145,16 +150,15 @@ class UserHistoryReconstructor {
145
150
  if (!dayStats[ticker]) this.initTickerStats(dayStats, ticker);
146
151
 
147
152
  const stats = dayStats[ticker];
148
- stats.isHolder = 1; // User is a holder today
153
+ stats.isHolder = 1;
149
154
  stats.holdCount++;
150
155
  stats.sumEntry += (trade.OpenRate || 0);
151
156
  stats.sumLev += (trade.Leverage || 1);
152
157
  }
153
158
 
154
- // Finalize Averages for this Day
159
+ // Finalize Averages
155
160
  for (const ticker in dayStats) {
156
161
  const stats = dayStats[ticker];
157
-
158
162
  if (stats.holdCount > 0) {
159
163
  stats.avgEntry = stats.sumEntry / stats.holdCount;
160
164
  stats.avgLeverage = stats.sumLev / stats.holdCount;
@@ -162,34 +166,27 @@ class UserHistoryReconstructor {
162
166
  if (stats.didBuy > 0) {
163
167
  stats.buyLeverage = stats.sumBuyLev / stats.didBuy;
164
168
  }
165
-
166
- // Cleanup temp sums
167
- delete stats.sumEntry;
168
- delete stats.sumLev;
169
- delete stats.sumBuyLev;
170
- delete stats.holdCount;
169
+ delete stats.sumEntry; delete stats.sumLev; delete stats.sumBuyLev; delete stats.holdCount;
171
170
  }
172
171
 
173
- // 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.
174
175
  if (Object.keys(dayStats).length > 0) {
175
- userTimeline[dateStr] = dayStats;
176
+ if (isEarliestRun) {
177
+ userTimeline[dateStr] = dayStats;
178
+ } else if (dateStr === currentExecutionDateStr) {
179
+ userTimeline[dateStr] = dayStats;
180
+ }
176
181
  }
177
182
  }
178
183
 
179
- // 4. Output the full timeline for this user
180
- // The ResultCommitter will handle splitting this into date buckets.
181
184
  this.results[user.id] = userTimeline;
182
185
  }
183
186
 
184
187
  initTickerStats(dayStats, ticker) {
185
188
  dayStats[ticker] = {
186
- isHolder: 0,
187
- didBuy: 0,
188
- didSell: 0,
189
- sumEntry: 0,
190
- sumLev: 0,
191
- holdCount: 0,
192
- sumBuyLev: 0,
189
+ isHolder: 0, didBuy: 0, didSell: 0, sumEntry: 0, sumLev: 0, holdCount: 0, sumBuyLev: 0,
193
190
  closeReasons: { "0": 0, "1": 0, "5": 0 }
194
191
  };
195
192
  }
@@ -49,8 +49,8 @@ class SqueezePotential {
49
49
 
50
50
  for (const ticker of tickers) {
51
51
  const shortSlAvg = signals.getMetric(computed, 'speculator-stop-loss-distance-by-ticker-short-long-breakdown', ticker, 'avg_short_sl_distance_pct');
52
- const userCount = signals.getMetric(computed, 'short-position-per-stock', ticker, 'short_count');
53
- const weight = signals.getMetric(computed, 'short-position-per-stock', ticker, 'total_short_exposure_weight');
52
+ const userCount = signals.getMetric(computed, 'short-position-per-stock', ticker, 'short_count');
53
+ const weight = signals.getMetric(computed, 'short-position-per-stock', ticker, 'total_short_exposure_weight');
54
54
 
55
55
  if (userCount > 0) {
56
56
  result[ticker] = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiden-shared-calculations-unified",
3
- "version": "1.0.107",
3
+ "version": "1.0.109",
4
4
  "description": "Shared calculation modules for the BullTrackers Computation System.",
5
5
  "main": "index.js",
6
6
  "files": [