aiden-shared-calculations-unified 1.0.18 → 1.0.19

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.
@@ -0,0 +1,246 @@
1
+ /**
2
+ * @fileoverview Backtest (Pass 4) calculation.
3
+ * Runs a full historical simulation of a trading strategy
4
+ * based on meta-signals.
5
+ */
6
+
7
+ // Note: Ensure this path is correct relative to your 'calculations' dir
8
+ const { loadAllPriceData } = require('../../utils/price_data_provider');
9
+
10
+ class StrategyPerformance {
11
+ constructor() {
12
+ this.INITIAL_CASH = 100000;
13
+ this.TRADE_SIZE_USD = 5000;
14
+
15
+ // --- Strategy Configuration ---
16
+ // Defines which signals from which computations trigger a BUY or SELL
17
+ this.strategySignals = {
18
+ 'smart-dumb-divergence-index': {
19
+ 'Capitulation': 'BUY',
20
+ 'Euphoria': 'SELL'
21
+ },
22
+ 'profit_cohort_divergence': {
23
+ 'Capitulation': 'BUY',
24
+ 'Profit Taking': 'SELL'
25
+ }
26
+ };
27
+ // --- End Configuration ---
28
+
29
+ this.priceMap = null;
30
+ }
31
+
32
+ /**
33
+ * Helper to find the first date a computation was stored.
34
+ * This determines the start date of the backtest.
35
+ * @param {object} db - Firestore instance
36
+ * @param {string} collection - resultsCollection
37
+ * @param {string} computation - computation name (e.g., 'smart-dumb-divergence-index')
38
+ * @param {string} category - computation category (e.g., 'meta')
39
+ * @returns {Promise<string|null>} YYYY-MM-DD string or null
40
+ */
41
+ async _findSignalInceptionDate(db, collection, computation, category) {
42
+ // Query for the oldest doc. This is a bit slow but runs once.
43
+ const snapshot = await db.collection(collection)
44
+ .where(`${category}.${computation}`, '==', true)
45
+ .orderBy(db.FieldPath.documentId(), 'asc')
46
+ .limit(1)
47
+ .get();
48
+
49
+ if (snapshot.empty) return null;
50
+ return snapshot.docs[0].id;
51
+ }
52
+
53
+ /**
54
+ * Fetches all required signals for the entire backtest period in one go.
55
+ */
56
+ async _fetchAllSignals(db, collection, resultsSub, compsSub, dates) {
57
+ const refs = [];
58
+ const signalMap = new Map();
59
+
60
+ for (const date of dates) {
61
+ for (const computation in this.strategySignals) {
62
+ const key = `${date}_${computation}`;
63
+
64
+ // Dynamically find category (this is a bit brittle, but works for your structure)
65
+ let category = 'meta'; // default
66
+ if (computation.includes('cohort')) category = 'behavioural';
67
+
68
+ const docRef = db.collection(collection).doc(date)
69
+ .collection(resultsSub).doc(category)
70
+ .collection(compsSub).doc(computation);
71
+ refs.push({ key, ref: docRef });
72
+ }
73
+ }
74
+
75
+ // This will be a large db.getAll call, but it's very efficient.
76
+ const snapshots = await db.getAll(...refs.map(r => r.ref));
77
+ snapshots.forEach((snap, idx) => {
78
+ if (snap.exists) signalMap.set(refs[idx].key, snap.data());
79
+ });
80
+ return signalMap;
81
+ }
82
+
83
+ /**
84
+ * Helper to find an instrument ID from a ticker.
85
+ * This is a simplified lookup from the priceMap.
86
+ */
87
+ _findInstrumentId(ticker) {
88
+ // This is inefficient, but will work. A reverse map would be faster.
89
+ for (const instrumentId in this.priceMap) {
90
+ const priceData = this.priceMap[instrumentId];
91
+ if (priceData && priceData.ticker && priceData.ticker === ticker) {
92
+ return instrumentId;
93
+ }
94
+ }
95
+ // Fallback for tickers that might have suffixes (e.g., from asset-crowd-flow)
96
+ for (const instrumentId in this.priceMap) {
97
+ const priceData = this.priceMap[instrumentId];
98
+ if (priceData && priceData.ticker && ticker.startsWith(priceData.ticker)) {
99
+ return instrumentId;
100
+ }
101
+ }
102
+ return null;
103
+ }
104
+
105
+
106
+ /**
107
+ * Main "meta-style" process function.
108
+ * @param {string} dateStr - Today's date.
109
+ * @param {object} dependencies - db, logger.
110
+ * @param {object} config - Computation config.
111
+ */
112
+ async process(dateStr, dependencies, config) {
113
+ const { db, logger } = dependencies;
114
+ const { resultsCollection, resultsSubcollection, computationsSubcollection } = config;
115
+
116
+ // 1. Load Price Data
117
+ if (!this.priceMap) {
118
+ logger.log('INFO', '[Backtest] Loading all price data for simulation...');
119
+ this.priceMap = await loadAllPriceData();
120
+ }
121
+
122
+ // 2. Find Backtest Start Date
123
+ // We find the oldest signal. In a real system, you'd find the *newest* of the "oldest" dates.
124
+ const inceptionDateStr = await this._findSignalInceptionDate(
125
+ db,
126
+ resultsCollection,
127
+ 'smart-dumb-divergence-index', // Our core signal
128
+ 'meta' // The category of this signal
129
+ );
130
+
131
+ if (!inceptionDateStr) {
132
+ logger.log('WARN', '[Backtest] No signal history found for smart-dumb-divergence-index. Skipping.');
133
+ return null;
134
+ }
135
+ logger.log('INFO', `[Backtest] Found signal inception date: ${inceptionDateStr}`);
136
+
137
+ // 3. Build Date Range
138
+ const allDates = [];
139
+ const current = new Date(inceptionDateStr + 'T00:00:00Z');
140
+ const end = new Date(dateStr + 'T00:00:00Z');
141
+ while (current <= end) {
142
+ allDates.push(current.toISOString().slice(0, 10));
143
+ current.setUTCDate(current.getUTCDate() + 1);
144
+ }
145
+
146
+ if (allDates.length < 2) {
147
+ logger.log('WARN', '[Backtest] Not enough history to run simulation.');
148
+ return null;
149
+ }
150
+
151
+ // 4. Fetch ALL signals for ALL dates in one go
152
+ logger.log('INFO', `[Backtest] Fetching ${allDates.length} days of signal data...`);
153
+ const signalDataMap = await this._fetchAllSignals(
154
+ db, resultsCollection, resultsSubcollection, computationsSubcollection, allDates
155
+ );
156
+
157
+ // 5. --- Run the Simulation Loop ---
158
+ const portfolio = { cash: this.INITIAL_CASH, positions: {} }; // { ticker: { shares, instrumentId, marketValue } }
159
+ const history = []; // To store daily portfolio value
160
+
161
+ for (const date of allDates) {
162
+ // A. Mark-to-Market existing positions
163
+ let portfolioValue = portfolio.cash;
164
+ for (const ticker in portfolio.positions) {
165
+ const pos = portfolio.positions[ticker];
166
+ const price = this.priceMap[pos.instrumentId]?.[date];
167
+
168
+ if (price) {
169
+ pos.marketValue = price * pos.shares;
170
+ portfolioValue += pos.marketValue;
171
+ } else {
172
+ portfolioValue += pos.marketValue; // Use last known value if price missing
173
+ }
174
+ }
175
+ history.push({ date, portfolioValue });
176
+
177
+ // B. Generate trades for *this* date
178
+ const tradesToMake = {}; // { 'AAPL': 'BUY', 'TSLA': 'SELL' }
179
+ for (const computation in this.strategySignals) {
180
+ const signalData = signalDataMap.get(`${date}_${computation}`);
181
+ if (!signalData) continue;
182
+
183
+ const signalRules = this.strategySignals[computation];
184
+ // The signalData is the *entire doc* (e.g., { "AAPL": { status: "Capitulation", ... } })
185
+ for (const ticker in signalData) {
186
+ const signal = signalData[ticker]?.status; // e.g., "Capitulation"
187
+ if (signalRules[signal]) {
188
+ tradesToMake[ticker] = signalRules[signal]; // 'BUY' or 'SELL'
189
+ }
190
+ }
191
+ }
192
+
193
+ // C. Execute Trades
194
+ for (const ticker in tradesToMake) {
195
+ const action = tradesToMake[ticker];
196
+
197
+ const instrumentId = this._findInstrumentId(ticker);
198
+ if (!instrumentId) {
199
+ // logger.log('WARN', `[Backtest] No instrumentId for ${ticker}`);
200
+ continue;
201
+ }
202
+
203
+ const price = this.priceMap[instrumentId]?.[date];
204
+ if (!price || price <= 0) {
205
+ // logger.log('WARN', `[Backtest] No price for ${ticker} on ${date}`);
206
+ continue;
207
+ }
208
+
209
+ if (action === 'BUY' && portfolio.cash >= this.TRADE_SIZE_USD) {
210
+ if (!portfolio.positions[ticker]) { // Only buy if not already holding
211
+ const shares = this.TRADE_SIZE_USD / price;
212
+ portfolio.cash -= this.TRADE_SIZE_USD;
213
+ portfolio.positions[ticker] = {
214
+ shares: shares,
215
+ instrumentId: instrumentId,
216
+ marketValue: this.TRADE_SIZE_USD
217
+ };
218
+ }
219
+ } else if (action === 'SELL' && portfolio.positions[ticker]) {
220
+ // Simple: sell all
221
+ portfolio.cash += portfolio.positions[ticker].marketValue;
222
+ delete portfolio.positions[ticker];
223
+ }
224
+ }
225
+ } // --- End Simulation Loop ---
226
+
227
+ const finalValue = history[history.length - 1]?.portfolioValue || this.INITIAL_CASH;
228
+ const totalReturnPct = ((finalValue - this.INITIAL_CASH) / this.INITIAL_CASH) * 100;
229
+
230
+ logger.log('INFO', `[Backtest] Simulation complete. Final Value: ${finalValue}, Return: ${totalReturnPct.toFixed(2)}%`);
231
+
232
+ return {
233
+ strategyName: 'SmartDumbDivergence_v1',
234
+ inceptionDate: inceptionDateStr,
235
+ endDate: dateStr,
236
+ finalPortfolioValue: finalValue,
237
+ totalReturnPercent: totalReturnPct,
238
+ dailyHistory: history // This can be plotted on the frontend
239
+ };
240
+ }
241
+
242
+ async getResult() { return null; }
243
+ reset() {}
244
+ }
245
+
246
+ module.exports = StrategyPerformance;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiden-shared-calculations-unified",
3
- "version": "1.0.18",
3
+ "version": "1.0.19",
4
4
  "description": "Shared calculation modules for the BullTrackers Computation System.",
5
5
  "main": "index.js",
6
6
  "files": [