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.
- package/functions/computation-system/onboarding.md +154 -869
- package/functions/computation-system/persistence/ResultCommitter.js +29 -12
- package/functions/computation-system/simulation/Fabricator.js +285 -0
- package/functions/computation-system/simulation/SeededRandom.js +41 -0
- package/functions/computation-system/simulation/SimRunner.js +51 -0
- package/functions/computation-system/tools/BuildReporter.js +147 -161
- package/package.json +1 -1
|
@@ -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;
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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;
|