bulltrackers-module 1.0.281 → 1.0.282

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,6 +4,7 @@
4
4
  * UPDATED: Implements Content-Based Hashing (ResultHash) for dependency short-circuiting.
5
5
  * UPDATED: Auto-enforces Weekend Mode validation.
6
6
  * UPDATED: Implements "Initial Write" logic to wipe stale data/shards on a fresh run.
7
+ * OPTIMIZED: Fetches pre-calculated 'simHash' from Registry (removes expensive simulation step).
7
8
  */
8
9
  const { commitBatchInChunks, generateDataHash } = require('../utils/utils');
9
10
  const { updateComputationStatus } = require('./StatusRepository');
@@ -18,6 +19,8 @@ const NON_RETRYABLE_ERRORS = [
18
19
  'PERMISSION_DENIED', 'DATA_LOSS', 'FAILED_PRECONDITION'
19
20
  ];
20
21
 
22
+ const SIMHASH_REGISTRY_COLLECTION = 'system_simhash_registry';
23
+
21
24
  /**
22
25
  * Commits results to Firestore.
23
26
  */
@@ -31,7 +34,7 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
31
34
 
32
35
  // Options defaults
33
36
  const flushMode = options.flushMode || 'STANDARD';
34
- const isInitialWrite = options.isInitialWrite === true; // [NEW] Flag for clean run
37
+ const isInitialWrite = options.isInitialWrite === true;
35
38
  const shardIndexes = options.shardIndexes || {};
36
39
  const nextShardIndexes = {};
37
40
 
@@ -80,6 +83,25 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
80
83
  const isEmpty = !result || (typeof result === 'object' && Object.keys(result).length === 0);
81
84
  const resultHash = isEmpty ? 'empty' : generateDataHash(result);
82
85
 
86
+ // [OPTIMIZATION] FETCH SimHash from Registry (Do NOT Calculate)
87
+ let simHash = null;
88
+ if (calc.manifest.hash && flushMode !== 'INTERMEDIATE') {
89
+ try {
90
+ // Fast O(1) lookup using Code Hash
91
+ // We simply check if the BuildReporter has already stamped this code version
92
+ const regDoc = await db.collection(SIMHASH_REGISTRY_COLLECTION).doc(calc.manifest.hash).get();
93
+ if (regDoc.exists) {
94
+ simHash = regDoc.data().simHash;
95
+ } else {
96
+ // Fallback: This happens if BuildReporter didn't run or is out of sync.
97
+ // We do NOT run SimRunner here to protect production performance.
98
+ logger.log('WARN', `[ResultCommitter] SimHash not found in registry for ${name} (Hash: ${calc.manifest.hash}). Is BuildReporter skipped?`);
99
+ }
100
+ } catch (regErr) {
101
+ logger.log('WARN', `[ResultCommitter] Failed to read SimHash registry: ${regErr.message}`);
102
+ }
103
+ }
104
+
83
105
  if (isEmpty) {
84
106
  if (flushMode === 'INTERMEDIATE') {
85
107
  nextShardIndexes[name] = currentShardIndex;
@@ -88,6 +110,7 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
88
110
  if (calc.manifest.hash) {
89
111
  successUpdates[name] = {
90
112
  hash: calc.manifest.hash,
113
+ simHash: simHash, // [NEW] Populated from Registry
91
114
  resultHash: resultHash,
92
115
  dependencyResultHashes: calc.manifest.dependencyResultHashes || {},
93
116
  category: calc.manifest.category,
@@ -115,8 +138,6 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
115
138
  .collection(config.computationsSubcollection)
116
139
  .doc(name);
117
140
 
118
- // Note: Multi-date fan-out rarely hits sharding, and tracking isInitialWrite per-date is complex.
119
- // We assume standard merging here.
120
141
  await writeSingleResult(dailyData, historicalDocRef, name, historicalDate, logger, config, deps, 0, 'STANDARD', false);
121
142
  }));
122
143
  await Promise.all(datePromises);
@@ -124,6 +145,7 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
124
145
  if (calc.manifest.hash) {
125
146
  successUpdates[name] = {
126
147
  hash: calc.manifest.hash,
148
+ simHash: simHash, // [NEW] Populated from Registry
127
149
  resultHash: resultHash,
128
150
  dependencyResultHashes: calc.manifest.dependencyResultHashes || {},
129
151
  category: calc.manifest.category,
@@ -151,6 +173,7 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
151
173
  if (calc.manifest.hash) {
152
174
  successUpdates[name] = {
153
175
  hash: calc.manifest.hash,
176
+ simHash: simHash, // [NEW] Populated from Registry
154
177
  resultHash: resultHash,
155
178
  dependencyResultHashes: calc.manifest.dependencyResultHashes || {},
156
179
  category: calc.manifest.category,
@@ -188,10 +211,8 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
188
211
 
189
212
  async function writeSingleResult(result, docRef, name, dateContext, logger, config, deps, startShardIndex = 0, flushMode = 'STANDARD', isInitialWrite = false) {
190
213
 
191
- // [NEW] Transition & Cleanup Logic
192
- // If this is the initial write of a run, we verify the existing state to prevent "Ghost Data".
214
+ // Transition & Cleanup Logic
193
215
  let wasSharded = false;
194
- let hadRootData = false;
195
216
  let shouldWipeShards = false;
196
217
 
197
218
  // Default: Merge updates. But if Initial Write, overwrite (merge: false) to clear stale fields.
@@ -203,11 +224,7 @@ async function writeSingleResult(result, docRef, name, dateContext, logger, conf
203
224
  if (currentSnap.exists) {
204
225
  const d = currentSnap.data();
205
226
  wasSharded = (d._sharded === true);
206
- // If it was sharded, we MUST wipe the old shards because we are re-writing from scratch.
207
- // Even if we write new shards, we want to ensure shard_10 doesn't persist if we only write up to shard_5.
208
227
  if (wasSharded) shouldWipeShards = true;
209
-
210
- // If it wasn't sharded, it had root data. overwriting (merge: false) handles that automatically.
211
228
  }
212
229
  } catch (e) { /* ignore read error */ }
213
230
  }
@@ -276,7 +293,7 @@ async function writeSingleResult(result, docRef, name, dateContext, logger, conf
276
293
  try {
277
294
  const updates = await prepareAutoShardedWrites(result, docRef, logger, constraints.bytes, constraints.keys, startShardIndex, flushMode);
278
295
 
279
- // [NEW] Inject Cleanup Ops
296
+ // Inject Cleanup Ops
280
297
  if (shouldWipeShards) {
281
298
  logger.log('INFO', `[Cleanup] ${name}: Wiping old shards before Write (Initial).`);
282
299
  const shardCol = docRef.collection('_shards');
@@ -410,4 +427,4 @@ function calculateFirestoreBytes(value) {
410
427
  if (typeof value === 'object') { let sum = 0; for (const k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { sum += (Buffer.byteLength(k, 'utf8') + 1) + calculateFirestoreBytes(value[k]); } } return sum; } return 0;
411
428
  }
412
429
 
413
- module.exports = { commitResults };
430
+ module.exports = { commitResults };
@@ -0,0 +1,285 @@
1
+ /**
2
+ * @fileoverview Generates deterministic mock contexts for Simulation Hashing.
3
+ * STRICTLY ALIGNED WITH SCHEMA.MD (Production V2 Schemas).
4
+ * UPGRADED: Supports Iteration (Seed Rotation) and Volume Scaling for Arrays.
5
+ */
6
+ const SeededRandom = require('./SeededRandom');
7
+ const { ContextFactory } = require('../context/ContextFactory');
8
+
9
+ const FAKE_SECTORS = ['Technology', 'Healthcare', 'Financials', 'Energy', 'Crypto', 'Consumer Discretionary'];
10
+ const FAKE_TICKERS = ['AAPL', 'GOOGL', 'MSFT', 'TSLA', 'AMZN', 'BTC', 'ETH', 'NVDA', 'META', 'AMD'];
11
+ const FAKE_TOPICS = ['AI', 'Earnings', 'Fed', 'Crypto', 'Macro'];
12
+
13
+ class Fabricator {
14
+ constructor(calcName) {
15
+ this.baseSeed = calcName;
16
+ // Primary RNG for high-level structure
17
+ this.rng = new SeededRandom(calcName);
18
+ }
19
+
20
+ /**
21
+ * Generates a context for a specific user iteration.
22
+ * @param {number} iteration - The index of the user in the batch (0, 1, 2...).
23
+ */
24
+ async generateContext(calcManifest, dependenciesManifest, iteration = 0) {
25
+ // [CRITICAL] Rotate the RNG state based on iteration so User 1 != User 2
26
+ this.rng = new SeededRandom(`${this.baseSeed}_ITER_${iteration}`);
27
+
28
+ const FIXED_DATE = '2025-01-01'; // Fixed simulation date
29
+
30
+ // 1. Generate Root Data
31
+ const user = this._generateUser(calcManifest.userType, iteration);
32
+ const insights = this._generateInsights(FIXED_DATE);
33
+
34
+ // 2. Generate Mock Dependencies (The "Schema Faking" Part)
35
+ const computed = {};
36
+ if (calcManifest.dependencies) {
37
+ for (const depName of calcManifest.dependencies) {
38
+ const depEntry = dependenciesManifest.get(depName);
39
+ if (depEntry && depEntry.class && depEntry.class.getSchema) {
40
+ const schema = depEntry.class.getSchema();
41
+ // [VOLUME UPGRADE] Dependencies usually represent aggregate data.
42
+ computed[depName] = this._fakeFromSchema(schema, true);
43
+ } else {
44
+ computed[depName] = {};
45
+ }
46
+ }
47
+ }
48
+
49
+ return ContextFactory.buildPerUserContext({
50
+ userId: user.id,
51
+ userType: user.type,
52
+ dateStr: FIXED_DATE,
53
+ todayPortfolio: user.portfolio.today,
54
+ yesterdayPortfolio: user.portfolio.yesterday,
55
+ todayHistory: user.history.today,
56
+ yesterdayHistory: user.history.yesterday,
57
+ metadata: calcManifest,
58
+ mappings: {
59
+ instrumentToTicker: this._generateMappings(),
60
+ instrumentToSector: this._generateSectorMappings()
61
+ },
62
+ insights: { today: insights },
63
+ socialData: { today: this._generateSocial(FIXED_DATE) },
64
+ computedDependencies: computed,
65
+ config: {},
66
+ deps: { logger: { log: () => {} } }
67
+ });
68
+ }
69
+
70
+ // --- Schema Faker Logic (Unchanged) ---
71
+ _fakeFromSchema(schema, isHighVolume = false) {
72
+ if (!schema) return {};
73
+ if (schema.type === 'object') {
74
+ const obj = {};
75
+ if (schema.properties) {
76
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
77
+ obj[key] = this._fakeFromSchema(propSchema, isHighVolume);
78
+ }
79
+ }
80
+ if (schema.patternProperties) {
81
+ const count = isHighVolume ? this.rng.range(20, 50) : 3;
82
+ const propSchema = Object.values(schema.patternProperties)[0];
83
+ for (let i = 0; i < count; i++) {
84
+ // Use deterministic ticker keys for stability
85
+ const key = `${this.rng.choice(FAKE_TICKERS)}`;
86
+ // Note: In real scenarios tickers are unique, so we might need a suffix if count > tickers.length
87
+ const safeKey = count > FAKE_TICKERS.length ? `${key}_${i}` : key;
88
+ obj[safeKey] = this._fakeFromSchema(propSchema, isHighVolume);
89
+ }
90
+ }
91
+ return obj;
92
+ }
93
+ if (schema.type === 'array') {
94
+ const min = isHighVolume ? 50 : 1;
95
+ const max = isHighVolume ? 150 : 5;
96
+ const len = this.rng.range(min, max);
97
+ return Array.from({ length: len }, () => this._fakeFromSchema(schema.items, isHighVolume));
98
+ }
99
+ if (schema.type === 'number') return parseFloat(this.rng.next().toFixed(4)) * 100;
100
+ if (schema.type === 'string') return "SIMULATED_STRING";
101
+ if (schema.type === 'boolean') return this.rng.bool();
102
+ return null;
103
+ }
104
+
105
+ // --- PROD ALIGNED GENERATORS ---
106
+
107
+ _generateUser(type, iteration) {
108
+ const userId = 1000000 + iteration; // Numeric ID to match Schema
109
+ const isSpeculator = (type === 'speculator');
110
+
111
+ return {
112
+ id: String(userId),
113
+ type: type || 'all',
114
+ portfolio: {
115
+ today: isSpeculator ? this._genSpecPortfolio(userId) : this._genNormalPortfolio(userId),
116
+ yesterday: isSpeculator ? this._genSpecPortfolio(userId) : this._genNormalPortfolio(userId)
117
+ },
118
+ history: {
119
+ today: { PublicHistoryPositions: this._genHistoryTrades(userId) },
120
+ yesterday: { PublicHistoryPositions: this._genHistoryTrades(userId) }
121
+ }
122
+ };
123
+ }
124
+
125
+ // Schema 2: Speculator User Portfolio
126
+ _genSpecPortfolio(userId) {
127
+ const invested = this.rng.range(5000, 50000);
128
+ const netProfit = this.rng.range(-20, 30);
129
+ const equity = invested * (1 + (netProfit / 100));
130
+
131
+ return {
132
+ AverageOpen: this.rng.range(100, 3000),
133
+ Equity: parseFloat(equity.toFixed(4)),
134
+ Invested: parseFloat(invested.toFixed(4)),
135
+ NetProfit: parseFloat(netProfit.toFixed(4)),
136
+ PublicPositions: Array.from({ length: this.rng.range(2, 10) }, (_, i) => {
137
+ const openRate = this.rng.range(50, 500);
138
+ const isBuy = this.rng.bool();
139
+ return {
140
+ Amount: parseFloat(this.rng.range(100, 1000).toFixed(4)),
141
+ CID: userId,
142
+ CurrentRate: parseFloat((openRate * (1 + (this.rng.next() - 0.5) * 0.1)).toFixed(2)),
143
+ InstrumentID: 100 + (i % 20),
144
+ IsBuy: isBuy,
145
+ IsTslEnabled: this.rng.bool(0.1),
146
+ Leverage: this.rng.choice([1, 2, 5, 10, 20]),
147
+ MirrorID: 0,
148
+ NetProfit: parseFloat(this.rng.range(-50, 50).toFixed(4)),
149
+ OpenDateTime: '2024-12-01T10:00:00Z',
150
+ OpenRate: parseFloat(openRate.toFixed(2)),
151
+ ParentPositionID: 0,
152
+ PipDifference: this.rng.range(-100, 100),
153
+ PositionID: 3000000000 + i,
154
+ StopLossRate: 0.01,
155
+ TakeProfitRate: 0
156
+ };
157
+ })
158
+ };
159
+ }
160
+
161
+ // Schema 1: Normal User Portfolio
162
+ _genNormalPortfolio(userId) {
163
+ const positions = Array.from({ length: this.rng.range(3, 12) }, (_, i) => ({
164
+ Direction: "Buy",
165
+ InstrumentID: 100 + (i % 20),
166
+ Invested: parseFloat(this.rng.range(5, 20).toFixed(4)), // Percent
167
+ NetProfit: parseFloat(this.rng.range(-30, 40).toFixed(4)),
168
+ Value: parseFloat(this.rng.range(5, 25).toFixed(4)) // Percent (Invested + PnL approx)
169
+ }));
170
+
171
+ // [CRITICAL] DataExtractor.getPortfolioDailyPnl uses AggregatedPositionsByInstrumentTypeID
172
+ // We must generate this aggregation or PnL calcs return 0.
173
+ const aggByType = positions.map(p => ({
174
+ Direction: p.Direction,
175
+ InstrumentTypeID: 5, // Stock
176
+ Invested: p.Invested,
177
+ NetProfit: p.NetProfit,
178
+ Value: p.Value
179
+ }));
180
+
181
+ return {
182
+ AggregatedMirrors: [],
183
+ AggregatedPositions: positions,
184
+ AggregatedPositionsByInstrumentTypeID: aggByType, // Required for PnL
185
+ AggregatedPositionsByStockIndustryID: [],
186
+ CreditByRealizedEquity: 0,
187
+ CreditByUnrealizedEquity: 0
188
+ };
189
+ }
190
+
191
+ // Schema 3: Trade History
192
+ _genHistoryTrades(userId) {
193
+ return Array.from({ length: this.rng.range(5, 30) }, (_, i) => ({
194
+ PositionID: 4000000000 + i,
195
+ CID: userId,
196
+ OpenDateTime: '2024-12-01T10:00:00Z',
197
+ OpenRate: 100.50,
198
+ InstrumentID: 100 + (i % 20),
199
+ IsBuy: this.rng.bool(),
200
+ MirrorID: 0,
201
+ ParentPositionID: 0,
202
+ CloseDateTime: '2024-12-02T10:00:00Z',
203
+ CloseRate: 110.20,
204
+ CloseReason: this.rng.choice([1, 5, 0]), // 1=SL, 5=TP, 0=Manual
205
+ ParentCID: userId,
206
+ NetProfit: parseFloat(this.rng.range(-50, 50).toFixed(4)),
207
+ Leverage: this.rng.choice([1, 2, 5])
208
+ }));
209
+ }
210
+
211
+ // Schema 5: Insights
212
+ // [CRITICAL FIX] Must return object { fetchedAt, insights: [] }, not just array.
213
+ _generateInsights(dateStr) {
214
+ const insightsArray = Array.from({ length: 50 }, (_, i) => ({
215
+ instrumentId: 100 + i,
216
+ total: this.rng.range(100, 50000), // Total owners
217
+ percentage: this.rng.next() * 0.05, // % of brokerage
218
+ growth: parseFloat((this.rng.next() * 10 - 5).toFixed(4)),
219
+ buy: this.rng.range(20, 95),
220
+ sell: 0, // Will calculate below
221
+ prevBuy: this.rng.range(20, 95),
222
+ prevSell: 0
223
+ }));
224
+
225
+ // Fix sell/prevSell math
226
+ insightsArray.forEach(i => {
227
+ i.sell = 100 - i.buy;
228
+ i.prevSell = 100 - i.prevBuy;
229
+ });
230
+
231
+ return {
232
+ fetchedAt: `${dateStr}T12:00:00Z`,
233
+ insights: insightsArray
234
+ };
235
+ }
236
+
237
+ // Schema 4: Social Post Data
238
+ // Returns Map: { "postId": { ... } }
239
+ _generateSocial(dateStr) {
240
+ const posts = {};
241
+ const count = this.rng.range(5, 20);
242
+
243
+ for(let i=0; i<count; i++) {
244
+ const id = `post_${i}_${this.rng.next().toString(36).substring(7)}`;
245
+ const ticker = this.rng.choice(FAKE_TICKERS);
246
+
247
+ posts[id] = {
248
+ commentCount: this.rng.range(0, 50),
249
+ createdAt: `${dateStr}T09:00:00Z`,
250
+ fetchedAt: `${dateStr}T10:00:00Z`,
251
+ fullText: `$${ticker} is looking bullish today!`,
252
+ language: 'en-gb',
253
+ likeCount: this.rng.range(0, 200),
254
+ postOwnerId: String(this.rng.range(100000, 999999)),
255
+ sentiment: {
256
+ overallSentiment: this.rng.choice(['Bullish', 'Bearish', 'Neutral']),
257
+ topics: [this.rng.choice(FAKE_TOPICS)]
258
+ },
259
+ textSnippet: `$${ticker} is looking...`,
260
+ tickers: [ticker]
261
+ };
262
+ }
263
+ return posts;
264
+ }
265
+
266
+ _generateMappings() {
267
+ const map = {};
268
+ // Map ID 100-150 to FAKE_TICKERS deterministically
269
+ for(let i=0; i<50; i++) {
270
+ // cycle through tickers
271
+ map[100+i] = FAKE_TICKERS[i % FAKE_TICKERS.length];
272
+ }
273
+ return map;
274
+ }
275
+
276
+ _generateSectorMappings() {
277
+ const map = {};
278
+ for(let i=0; i<50; i++) {
279
+ map[100+i] = FAKE_SECTORS[i % FAKE_SECTORS.length];
280
+ }
281
+ return map;
282
+ }
283
+ }
284
+
285
+ module.exports = Fabricator;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @fileoverview Deterministic Pseudo-Random Number Generator (LCG).
3
+ * Ensures that for a given seed, the sequence of numbers is identical across runs.
4
+ */
5
+ class SeededRandom {
6
+ constructor(seedString) {
7
+ this.state = this._stringToSeed(seedString);
8
+ }
9
+
10
+ _stringToSeed(str) {
11
+ let h = 2166136261 >>> 0;
12
+ for (let i = 0; i < str.length; i++) {
13
+ h = Math.imul(h ^ str.charCodeAt(i), 16777619);
14
+ }
15
+ return h >>> 0;
16
+ }
17
+
18
+ /** Returns a float between 0 and 1 */
19
+ next() {
20
+ this.state = (Math.imul(48271, this.state) % 2147483647);
21
+ return (this.state - 1) / 2147483646;
22
+ }
23
+
24
+ /** Returns an integer between min and max (inclusive) */
25
+ range(min, max) {
26
+ return Math.floor(this.next() * (max - min + 1)) + min;
27
+ }
28
+
29
+ /** Returns a random element from an array */
30
+ choice(arr) {
31
+ if (!arr || arr.length === 0) return null;
32
+ return arr[this.range(0, arr.length - 1)];
33
+ }
34
+
35
+ /** Returns a boolean based on probability */
36
+ bool(probability = 0.5) {
37
+ return this.next() < probability;
38
+ }
39
+ }
40
+
41
+ module.exports = SeededRandom;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @fileoverview Runner for Behavioral Hashing (SimHash).
3
+ * Executes a calculation against a fabricated, deterministic context.
4
+ */
5
+ const Fabricator = require('./Fabricator');
6
+ const { generateDataHash } = require('../utils/utils');
7
+
8
+ class SimRunner {
9
+ /**
10
+ * Runs the simulation for a specific calculation.
11
+ * @param {Object} calcManifest - The manifest entry for the calculation.
12
+ * @param {Map} fullManifestMap - Map of all manifests (to look up dependencies).
13
+ * @returns {Promise<string>} The SimHash (SHA256 of the output).
14
+ */
15
+ static async run(calcManifest, fullManifestMap) {
16
+ try {
17
+ const fabricator = new Fabricator(calcManifest.name);
18
+
19
+ // 1. Generate Deterministic Context
20
+ const context = await fabricator.generateContext(calcManifest, fullManifestMap);
21
+
22
+ // 2. Instantiate
23
+ const instance = new calcManifest.class();
24
+
25
+ // 3. Process
26
+ await instance.process(context);
27
+
28
+ // 4. Get Result
29
+ // Note: If the calculation uses internal state buffering (like `results` property),
30
+ // getResult() usually returns that.
31
+ let result = null;
32
+ if (instance.getResult) {
33
+ result = await instance.getResult();
34
+ } else {
35
+ result = instance.result || instance.results || {};
36
+ }
37
+
38
+ // 5. Sanitize & Hash
39
+ // We strip any non-deterministic keys if they leak (like timestamps generated inside process)
40
+ // But ideally, the context mocking prevents this.
41
+ return generateDataHash(result);
42
+
43
+ } catch (e) {
44
+ console.error(`[SimRunner] Simulation failed for ${calcManifest.name}:`, e);
45
+ // If simulation crashes, we return a hash of the error to safely trigger a re-run
46
+ return generateDataHash({ error: e.message });
47
+ }
48
+ }
49
+ }
50
+
51
+ module.exports = SimRunner;