aiden-shared-calculations-unified 1.0.18 → 1.0.20
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;
|
|
@@ -10,7 +10,7 @@ class CapitalDeploymentStrategy {
|
|
|
10
10
|
constructor() {
|
|
11
11
|
this.lookbackDays = 7;
|
|
12
12
|
this.correlationWindow = 3; // How many days after a signal to link behavior
|
|
13
|
-
this.depositSignalThreshold = -
|
|
13
|
+
this.depositSignalThreshold = -0.005; // Formerly -1.0
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
_getDateStr(baseDate, daysAgo) {
|
|
@@ -11,7 +11,7 @@ class CashFlowLiquidation {
|
|
|
11
11
|
this.lookbackDays = 7;
|
|
12
12
|
this.correlationWindow = 3;
|
|
13
13
|
// A positive value signals a net crowd withdrawal
|
|
14
|
-
this.withdrawalSignalThreshold = 1.0
|
|
14
|
+
this.withdrawalSignalThreshold = 0.005; // Formerly 1.0
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
_getDateStr(baseDate, daysAgo) {
|