aiden-shared-calculations-unified 1.0.108 → 1.0.110
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/calculations/core/insights-daily-bought-vs-sold-count.js +20 -15
- package/calculations/core/insights-daily-ownership-delta.js +12 -10
- package/calculations/core/instrument-price-change-1d.js +16 -31
- package/calculations/core/instrument-price-momentum-20d.js +16 -26
- package/calculations/core/ownership-vs-performance-ytd.js +15 -52
- package/calculations/core/ownership-vs-volatility.js +27 -36
- package/calculations/core/platform-daily-bought-vs-sold-count.js +28 -26
- package/calculations/core/platform-daily-ownership-delta.js +28 -31
- package/calculations/core/price-metrics.js +15 -54
- package/calculations/core/short-interest-growth.js +6 -13
- package/calculations/core/trending-ownership-momentum.js +16 -28
- package/calculations/core/user-history-reconstructor.js +48 -49
- package/calculations/gauss/cohort-capital-flow.js +34 -71
- package/calculations/gauss/cohort-definer.js +61 -142
- package/calculations/gem/cohort-momentum-state.js +27 -77
- package/calculations/gem/skilled-cohort-flow.js +36 -114
- package/calculations/gem/unskilled-cohort-flow.js +36 -112
- package/calculations/ghost-book/retail-gamma-exposure.js +14 -61
- package/calculations/helix/herd-consensus-score.js +27 -90
- package/calculations/helix/winner-loser-flow.js +21 -90
- package/calculations/predicative-alpha/cognitive-dissonance.js +25 -91
- package/calculations/predicative-alpha/diamond-hand-fracture.js +17 -72
- package/calculations/predicative-alpha/mimetic-latency.js +21 -100
- package/package.json +1 -1
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Calculation (Pass 1) for ownership delta (users added/lost per asset).
|
|
3
|
-
* REFACTORED: Tracks net change in number of owners. ---> TODO IN TEST ENVIRONMENT THE VALUE OF NUMBER OF CLOSURES ALWAYS RETURNS 0, IS THIS A COMPUTATION BUG OR A TEST HARNESS BUG?
|
|
4
|
-
*/
|
|
5
1
|
class PlatformDailyOwnershipDelta {
|
|
6
2
|
constructor() {
|
|
7
|
-
this.assetChanges = new Map();
|
|
3
|
+
this.assetChanges = new Map();
|
|
8
4
|
this.tickerMap = null;
|
|
9
5
|
}
|
|
10
6
|
|
|
@@ -26,16 +22,17 @@ class PlatformDailyOwnershipDelta {
|
|
|
26
22
|
"properties": {
|
|
27
23
|
"owners_added": { "type": "number" },
|
|
28
24
|
"owners_removed": { "type": "number" },
|
|
29
|
-
"net_ownership_change": { "type": "number" }
|
|
25
|
+
"net_ownership_change": { "type": "number" },
|
|
26
|
+
"total_owners_today": { "type": "number" }
|
|
30
27
|
},
|
|
31
|
-
"required": ["owners_added", "owners_removed", "net_ownership_change"]
|
|
28
|
+
"required": ["owners_added", "owners_removed", "net_ownership_change", "total_owners_today"]
|
|
32
29
|
};
|
|
33
30
|
return { "type": "object", "patternProperties": { "^.*$": tickerSchema } };
|
|
34
31
|
}
|
|
35
32
|
|
|
36
33
|
_initAsset(instId) {
|
|
37
34
|
if (!this.assetChanges.has(instId)) {
|
|
38
|
-
this.assetChanges.set(instId, { added: 0, removed: 0 });
|
|
35
|
+
this.assetChanges.set(instId, { added: 0, removed: 0, total: 0 });
|
|
39
36
|
}
|
|
40
37
|
}
|
|
41
38
|
|
|
@@ -53,28 +50,29 @@ class PlatformDailyOwnershipDelta {
|
|
|
53
50
|
const { mappings, user } = context;
|
|
54
51
|
if (!this.tickerMap) this.tickerMap = mappings.instrumentToTicker;
|
|
55
52
|
|
|
56
|
-
// If either portfolio is missing, we can't calculate delta for this user
|
|
57
|
-
if (!user.portfolio.today || !user.portfolio.yesterday) return;
|
|
58
|
-
|
|
59
53
|
const todayPositions = extract.getPositions(user.portfolio.today, user.type);
|
|
60
|
-
const yesterdayPositions = extract.getPositions(user.portfolio.yesterday, user.type);
|
|
61
|
-
|
|
62
54
|
const tSet = this._getOwnedInstruments(todayPositions, extract);
|
|
63
|
-
const ySet = this._getOwnedInstruments(yesterdayPositions, extract);
|
|
64
55
|
|
|
65
|
-
//
|
|
56
|
+
// Establish current total for Baseline
|
|
66
57
|
for (const instId of tSet) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
this.assetChanges.get(instId).added++;
|
|
70
|
-
}
|
|
58
|
+
this._initAsset(instId);
|
|
59
|
+
this.assetChanges.get(instId).total++;
|
|
71
60
|
}
|
|
72
61
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
62
|
+
if (user.portfolio.yesterday) {
|
|
63
|
+
const yesterdayPositions = extract.getPositions(user.portfolio.yesterday, user.type);
|
|
64
|
+
const ySet = this._getOwnedInstruments(yesterdayPositions, extract);
|
|
65
|
+
for (const instId of tSet) {
|
|
66
|
+
if (!ySet.has(instId)) {
|
|
67
|
+
this._initAsset(instId);
|
|
68
|
+
this.assetChanges.get(instId).added++;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for (const instId of ySet) {
|
|
72
|
+
if (!tSet.has(instId)) {
|
|
73
|
+
this._initAsset(instId);
|
|
74
|
+
this.assetChanges.get(instId).removed++;
|
|
75
|
+
}
|
|
78
76
|
}
|
|
79
77
|
}
|
|
80
78
|
}
|
|
@@ -84,13 +82,12 @@ class PlatformDailyOwnershipDelta {
|
|
|
84
82
|
const result = {};
|
|
85
83
|
for (const [instId, data] of this.assetChanges.entries()) {
|
|
86
84
|
const ticker = this.tickerMap[instId] || `id_${instId}`;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
85
|
+
result[ticker] = {
|
|
86
|
+
owners_added: data.added,
|
|
87
|
+
owners_removed: data.removed,
|
|
88
|
+
net_ownership_change: data.added - data.removed,
|
|
89
|
+
total_owners_today: data.total
|
|
90
|
+
};
|
|
94
91
|
}
|
|
95
92
|
return result;
|
|
96
93
|
}
|
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Calculation (Pass 1 - Meta) for historical price metrics.
|
|
3
|
-
* FIXED: Supports Sharded/Batched Execution.
|
|
4
|
-
* MOVED: Sector aggregation logic moved to getResult() to handle sharding correctly.
|
|
5
|
-
*/
|
|
6
1
|
const RANGES = [7, 30, 90, 365];
|
|
7
2
|
const TRADING_DAYS_PER_YEAR = 252;
|
|
8
3
|
|
|
9
4
|
class CorePriceMetrics {
|
|
10
5
|
constructor() {
|
|
11
6
|
this.result = { by_instrument: {}, by_sector: {} };
|
|
12
|
-
// We persist mappings here because we process shard-by-shard
|
|
13
7
|
this.mappings = null;
|
|
14
8
|
}
|
|
15
9
|
|
|
@@ -49,13 +43,11 @@ class CorePriceMetrics {
|
|
|
49
43
|
const prices = [];
|
|
50
44
|
let currentDate = new Date(endDateStr + 'T00:00:00Z');
|
|
51
45
|
let lastPrice = null;
|
|
52
|
-
|
|
53
46
|
for (let i = 0; i < numDays; i++) {
|
|
54
47
|
const targetDateStr = currentDate.toISOString().slice(0, 10);
|
|
55
48
|
let price = this._findPriceOnOrBefore(priceHistoryObj, targetDateStr);
|
|
56
49
|
if (price === null) price = lastPrice;
|
|
57
50
|
else lastPrice = price;
|
|
58
|
-
|
|
59
51
|
if (price !== null) prices.push(price);
|
|
60
52
|
currentDate.setUTCDate(currentDate.getUTCDate() - 1);
|
|
61
53
|
}
|
|
@@ -63,9 +55,9 @@ class CorePriceMetrics {
|
|
|
63
55
|
}
|
|
64
56
|
|
|
65
57
|
_calculateStats(priceArray) {
|
|
66
|
-
|
|
58
|
+
const currentPrice = priceArray[priceArray.length - 1] || null;
|
|
59
|
+
if (priceArray.length < 2) return { stdDev: null, mean: null, drawdown: null, currentPrice };
|
|
67
60
|
|
|
68
|
-
// Drawdown
|
|
69
61
|
let maxDrawdown = 0, peak = -Infinity;
|
|
70
62
|
for (const price of priceArray) {
|
|
71
63
|
if (price > peak) peak = price;
|
|
@@ -74,8 +66,6 @@ class CorePriceMetrics {
|
|
|
74
66
|
if (dd < maxDrawdown) maxDrawdown = dd;
|
|
75
67
|
}
|
|
76
68
|
}
|
|
77
|
-
|
|
78
|
-
// Returns & StdDev
|
|
79
69
|
const returns = [];
|
|
80
70
|
for (let i = 1; i < priceArray.length; i++) {
|
|
81
71
|
const prev = priceArray[i - 1];
|
|
@@ -84,78 +74,53 @@ class CorePriceMetrics {
|
|
|
84
74
|
}
|
|
85
75
|
const mean = returns.reduce((a, b) => a + b, 0) / returns.length;
|
|
86
76
|
const variance = returns.reduce((a, b) => a + (b - mean) ** 2, 0) / (returns.length - 1);
|
|
87
|
-
|
|
88
|
-
return { stdDev: Math.sqrt(variance), mean, drawdown: maxDrawdown };
|
|
77
|
+
return { stdDev: Math.sqrt(variance), mean, drawdown: maxDrawdown, currentPrice };
|
|
89
78
|
}
|
|
90
79
|
|
|
91
80
|
process(context) {
|
|
92
81
|
const { mappings, prices, date } = context;
|
|
93
82
|
const { instrumentToTicker } = mappings;
|
|
94
|
-
|
|
95
|
-
// Save mappings for the final aggregation step
|
|
96
83
|
if (!this.mappings) this.mappings = mappings;
|
|
97
|
-
|
|
98
84
|
const priceData = prices?.history;
|
|
99
85
|
const todayDateStr = date.today;
|
|
86
|
+
if (!priceData || !todayDateStr) return;
|
|
100
87
|
|
|
101
|
-
if (!priceData || !todayDateStr) {
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Iterate over THIS SHARD'S data
|
|
106
88
|
for (const p of Object.values(priceData)) {
|
|
107
89
|
const ticker = instrumentToTicker[p.instrumentId];
|
|
108
90
|
if (!ticker) continue;
|
|
109
|
-
|
|
110
|
-
const metrics = {};
|
|
91
|
+
const metrics = { baseline_price: null };
|
|
111
92
|
for (const range of RANGES) {
|
|
112
93
|
const priceArray = this._getHistoricalPriceArray(p, todayDateStr, range + 1);
|
|
113
94
|
const stats = this._calculateStats(priceArray);
|
|
114
|
-
|
|
115
|
-
metrics[`stdev_${range}d`] = stats.stdDev;
|
|
116
|
-
metrics[`max_drawdown_${range}d`] = stats.drawdown;
|
|
117
|
-
metrics[`volatility_annualized_${range}d`] = stats.stdDev * Math.sqrt(TRADING_DAYS_PER_YEAR);
|
|
118
|
-
metrics[`sharpe_ratio_${range}d`] = stats.stdDev > 0
|
|
119
|
-
? (stats.mean / stats.stdDev) * Math.sqrt(TRADING_DAYS_PER_YEAR)
|
|
120
|
-
: 0;
|
|
95
|
+
metrics.baseline_price = stats.currentPrice;
|
|
96
|
+
metrics[`stdev_${range}d`] = (stats.stdDev !== null && isFinite(stats.stdDev)) ? stats.stdDev : null;
|
|
97
|
+
metrics[`max_drawdown_${range}d`] = (stats.drawdown !== null && isFinite(stats.drawdown)) ? stats.drawdown : null;
|
|
98
|
+
metrics[`volatility_annualized_${range}d`] = (stats.stdDev !== null) ? stats.stdDev * Math.sqrt(TRADING_DAYS_PER_YEAR) : null;
|
|
99
|
+
metrics[`sharpe_ratio_${range}d`] = (stats.stdDev > 0) ? (stats.mean / stats.stdDev) * Math.sqrt(TRADING_DAYS_PER_YEAR) : null;
|
|
121
100
|
}
|
|
122
|
-
// Accumulate into by_instrument
|
|
123
101
|
this.result.by_instrument[ticker] = metrics;
|
|
124
102
|
}
|
|
125
103
|
}
|
|
126
104
|
|
|
127
105
|
async getResult() {
|
|
128
|
-
// Perform Sector Aggregation HERE (after all shards are processed)
|
|
129
106
|
const by_instrument = this.result.by_instrument;
|
|
130
107
|
const instrumentToSector = this.mappings?.instrumentToSector || {};
|
|
131
108
|
const instrumentToTicker = this.mappings?.instrumentToTicker || {};
|
|
132
|
-
|
|
133
|
-
// Reverse map ticker -> instrumentId
|
|
134
109
|
const tickerToInstrument = {};
|
|
135
|
-
for(const [id, tick] of Object.entries(instrumentToTicker))
|
|
136
|
-
tickerToInstrument[tick] = id;
|
|
137
|
-
}
|
|
110
|
+
for(const [id, tick] of Object.entries(instrumentToTicker)) tickerToInstrument[tick] = id;
|
|
138
111
|
|
|
139
112
|
const sectorAggs = {};
|
|
140
113
|
for (const ticker in by_instrument) {
|
|
141
114
|
const instId = tickerToInstrument[ticker];
|
|
142
115
|
const sector = instrumentToSector[instId] || "Unknown";
|
|
143
|
-
|
|
144
116
|
if (!sectorAggs[sector]) sectorAggs[sector] = { metrics: {}, counts: {} };
|
|
145
|
-
|
|
146
117
|
const data = by_instrument[ticker];
|
|
147
118
|
for (const key in data) {
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
if (data[key] !== null) {
|
|
153
|
-
sectorAggs[sector].metrics[key] += data[key];
|
|
154
|
-
sectorAggs[sector].counts[key]++;
|
|
155
|
-
}
|
|
119
|
+
if (key === 'baseline_price') continue;
|
|
120
|
+
if (!sectorAggs[sector].metrics[key]) { sectorAggs[sector].metrics[key] = 0; sectorAggs[sector].counts[key] = 0; }
|
|
121
|
+
if (data[key] !== null) { sectorAggs[sector].metrics[key] += data[key]; sectorAggs[sector].counts[key]++; }
|
|
156
122
|
}
|
|
157
123
|
}
|
|
158
|
-
|
|
159
124
|
const by_sector = {};
|
|
160
125
|
for (const sector in sectorAggs) {
|
|
161
126
|
by_sector[sector] = {};
|
|
@@ -164,14 +129,10 @@ class CorePriceMetrics {
|
|
|
164
129
|
by_sector[sector][`average_${key}`] = count > 0 ? sectorAggs[sector].metrics[key] / count : null;
|
|
165
130
|
}
|
|
166
131
|
}
|
|
167
|
-
|
|
168
132
|
this.result.by_sector = by_sector;
|
|
169
133
|
return this.result;
|
|
170
134
|
}
|
|
171
135
|
|
|
172
|
-
reset() {
|
|
173
|
-
this.result = { by_instrument: {}, by_sector: {} };
|
|
174
|
-
this.mappings = null;
|
|
175
|
-
}
|
|
136
|
+
reset() { this.result = { by_instrument: {}, by_sector: {} }; this.mappings = null; }
|
|
176
137
|
}
|
|
177
138
|
module.exports = CorePriceMetrics;
|
|
@@ -19,7 +19,7 @@ class ShortInterestGrowth {
|
|
|
19
19
|
"TICKER": {
|
|
20
20
|
"shortCount": 0,
|
|
21
21
|
"shortSparkline": [0, 0, 0, 0, 0, 0, 0],
|
|
22
|
-
"growth7d":
|
|
22
|
+
"growth7d": { "type": ["number", "null"] }
|
|
23
23
|
}
|
|
24
24
|
};
|
|
25
25
|
}
|
|
@@ -27,7 +27,6 @@ class ShortInterestGrowth {
|
|
|
27
27
|
async process(context) {
|
|
28
28
|
const { insights: insightsHelper } = context.math;
|
|
29
29
|
const { previousComputed, mappings } = context;
|
|
30
|
-
|
|
31
30
|
const dailyInsights = insightsHelper.getInsights(context, 'today');
|
|
32
31
|
const previousState = previousComputed['short-interest-growth'] || {};
|
|
33
32
|
|
|
@@ -36,34 +35,28 @@ class ShortInterestGrowth {
|
|
|
36
35
|
const ticker = mappings.instrumentToTicker[instId];
|
|
37
36
|
if (!ticker) continue;
|
|
38
37
|
|
|
39
|
-
// Calculate Short Count: Total Owners * (Sell % / 100)
|
|
40
38
|
const shortCount = insightsHelper.getShortCount(insight);
|
|
41
|
-
|
|
42
39
|
const prevState = previousState[ticker] || { shortSparkline: [] };
|
|
43
40
|
const sparkline = [...(prevState.shortSparkline || [])];
|
|
44
|
-
|
|
45
41
|
sparkline.push(shortCount);
|
|
46
42
|
if (sparkline.length > 7) sparkline.shift();
|
|
47
43
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (sparkline.length > 1) {
|
|
44
|
+
let growth7d = null;
|
|
45
|
+
if (sparkline.length >= 7) {
|
|
51
46
|
const start = sparkline[0];
|
|
52
47
|
const end = sparkline[sparkline.length - 1];
|
|
53
|
-
if (start > 0)
|
|
54
|
-
growth7d = ((end - start) / start) * 100;
|
|
55
|
-
}
|
|
48
|
+
if (start > 0) growth7d = ((end - start) / start) * 100;
|
|
56
49
|
}
|
|
57
50
|
|
|
58
51
|
this.results[ticker] = {
|
|
59
52
|
shortCount,
|
|
60
53
|
shortSparkline: sparkline,
|
|
61
|
-
growth7d
|
|
54
|
+
growth7d: (growth7d !== null && isFinite(growth7d)) ? growth7d : null
|
|
62
55
|
};
|
|
63
56
|
}
|
|
64
57
|
}
|
|
65
58
|
|
|
66
59
|
async getResult() { return this.results; }
|
|
60
|
+
reset() { this.results = {}; }
|
|
67
61
|
}
|
|
68
|
-
|
|
69
62
|
module.exports = ShortInterestGrowth;
|
|
@@ -7,7 +7,7 @@ class TrendingOwnershipMomentum {
|
|
|
7
7
|
type: 'meta',
|
|
8
8
|
category: 'core',
|
|
9
9
|
userType: 'n/a',
|
|
10
|
-
isHistorical: true,
|
|
10
|
+
isHistorical: true,
|
|
11
11
|
rootDataDependencies: ['insights']
|
|
12
12
|
};
|
|
13
13
|
}
|
|
@@ -18,9 +18,9 @@ class TrendingOwnershipMomentum {
|
|
|
18
18
|
return {
|
|
19
19
|
"TICKER": {
|
|
20
20
|
"currentOwners": 0,
|
|
21
|
-
"sparkline": [0, 0, 0, 0, 0, 0, 0],
|
|
22
|
-
"momentumScore":
|
|
23
|
-
"trend": "string"
|
|
21
|
+
"sparkline": [0, 0, 0, 0, 0, 0, 0],
|
|
22
|
+
"momentumScore": { "type": ["number", "null"] },
|
|
23
|
+
"trend": "string"
|
|
24
24
|
}
|
|
25
25
|
};
|
|
26
26
|
}
|
|
@@ -28,12 +28,8 @@ class TrendingOwnershipMomentum {
|
|
|
28
28
|
async process(context) {
|
|
29
29
|
const { insights: insightsHelper } = context.math;
|
|
30
30
|
const { previousComputed, mappings } = context;
|
|
31
|
-
|
|
32
|
-
// 1. Get Today's Data
|
|
33
31
|
const dailyInsights = insightsHelper.getInsights(context, 'today');
|
|
34
|
-
if (!dailyInsights.length) return;
|
|
35
|
-
|
|
36
|
-
// 2. Get Yesterday's State (for rolling history)
|
|
32
|
+
if (!dailyInsights || !dailyInsights.length) return;
|
|
37
33
|
const previousState = previousComputed['trending-ownership-momentum'] || {};
|
|
38
34
|
|
|
39
35
|
for (const insight of dailyInsights) {
|
|
@@ -43,45 +39,37 @@ class TrendingOwnershipMomentum {
|
|
|
43
39
|
|
|
44
40
|
const currentCount = insightsHelper.getTotalOwners(insight);
|
|
45
41
|
const prevState = previousState[ticker] || { sparkline: [] };
|
|
46
|
-
|
|
47
|
-
// 3. Update Rolling Window (Last 7 Days)
|
|
48
|
-
// Create new array copy to avoid mutation issues
|
|
49
42
|
const sparkline = [...(prevState.sparkline || [])];
|
|
50
43
|
sparkline.push(currentCount);
|
|
51
|
-
|
|
52
|
-
// Keep only last 7 entries
|
|
53
44
|
if (sparkline.length > 7) sparkline.shift();
|
|
54
45
|
|
|
55
|
-
|
|
56
|
-
let
|
|
57
|
-
|
|
46
|
+
let momentumScore = null;
|
|
47
|
+
let trend = 'WARM_UP';
|
|
48
|
+
|
|
49
|
+
if (sparkline.length >= 7) {
|
|
58
50
|
const n = sparkline.length;
|
|
59
51
|
let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0;
|
|
60
52
|
for (let i = 0; i < n; i++) {
|
|
61
|
-
sumX += i;
|
|
62
|
-
sumY += sparkline[i];
|
|
63
|
-
sumXY += i * sparkline[i];
|
|
64
|
-
sumXX += i * i;
|
|
53
|
+
sumX += i; sumY += sparkline[i]; sumXY += i * sparkline[i]; sumXX += i * i;
|
|
65
54
|
}
|
|
66
55
|
const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
|
|
67
|
-
// Normalize slope by current count to get percentage-like momentum
|
|
68
56
|
momentumScore = currentCount > 0 ? (slope / currentCount) * 100 : 0;
|
|
57
|
+
|
|
58
|
+
trend = 'STABLE';
|
|
59
|
+
if (momentumScore > 0.5) trend = 'RISING';
|
|
60
|
+
if (momentumScore < -0.5) trend = 'FALLING';
|
|
69
61
|
}
|
|
70
62
|
|
|
71
|
-
let trend = 'STABLE';
|
|
72
|
-
if (momentumScore > 0.5) trend = 'RISING';
|
|
73
|
-
if (momentumScore < -0.5) trend = 'FALLING';
|
|
74
|
-
|
|
75
63
|
this.results[ticker] = {
|
|
76
64
|
currentOwners: currentCount,
|
|
77
65
|
sparkline,
|
|
78
|
-
momentumScore,
|
|
66
|
+
momentumScore: (momentumScore !== null && isFinite(momentumScore)) ? momentumScore : null,
|
|
79
67
|
trend
|
|
80
68
|
};
|
|
81
69
|
}
|
|
82
70
|
}
|
|
83
71
|
|
|
84
72
|
async getResult() { return this.results; }
|
|
73
|
+
reset() { this.results = {}; }
|
|
85
74
|
}
|
|
86
|
-
|
|
87
75
|
module.exports = TrendingOwnershipMomentum;
|
|
@@ -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
|
|
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
|
|
48
|
-
"^\\d{4}-\\d{2}-\\d{2}$": {
|
|
49
|
+
"^\\d{4}-\\d{2}-\\d{2}$": { // Date Key
|
|
49
50
|
"type": "object",
|
|
50
51
|
"patternProperties": {
|
|
51
|
-
|
|
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.
|
|
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
|
-
//
|
|
69
|
-
const events = [];
|
|
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,42 +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
97
|
if (closeTime) { events.push({ time: closeTime, type: 'CLOSE', trade, ticker }); }
|
|
86
98
|
}
|
|
87
99
|
|
|
88
|
-
// Sort events by time
|
|
89
100
|
events.sort((a, b) => a.time - b.time);
|
|
90
|
-
|
|
91
101
|
if (events.length === 0) return;
|
|
92
102
|
|
|
93
|
-
//
|
|
94
|
-
const userTimeline = {};
|
|
103
|
+
// 4. Scan-Line Execution (Replay History)
|
|
104
|
+
const userTimeline = {};
|
|
105
|
+
const activePositions = new Map();
|
|
95
106
|
|
|
96
|
-
// Tracking State
|
|
97
|
-
const activePositions = new Map(); // Map<PositionID, Trade>
|
|
98
|
-
|
|
99
|
-
// Determine Start and End dates for the loop
|
|
100
107
|
const firstEventTime = events[0].time;
|
|
101
108
|
const lastEventTime = events[events.length - 1].time;
|
|
102
109
|
|
|
103
110
|
const startDate = new Date(firstEventTime); startDate.setUTCHours(0,0,0,0);
|
|
104
|
-
const endDate = new Date(lastEventTime);
|
|
111
|
+
const endDate = new Date(lastEventTime); endDate.setUTCHours(0,0,0,0);
|
|
105
112
|
|
|
106
113
|
let currentEventIdx = 0;
|
|
107
114
|
const oneDayMs = 86400000;
|
|
108
115
|
|
|
109
|
-
// Iterate day by day
|
|
116
|
+
// Iterate day by day
|
|
110
117
|
for (let d = startDate.getTime(); d <= endDate.getTime(); d += oneDayMs) {
|
|
111
118
|
const dateStr = new Date(d).toISOString().slice(0, 10);
|
|
112
119
|
const dayEnd = d + oneDayMs - 1;
|
|
113
120
|
|
|
114
|
-
const dayStats = {};
|
|
121
|
+
const dayStats = {};
|
|
115
122
|
|
|
116
|
-
// Process
|
|
123
|
+
// Process events for TODAY
|
|
117
124
|
while (currentEventIdx < events.length && events[currentEventIdx].time <= dayEnd) {
|
|
118
125
|
const event = events[currentEventIdx];
|
|
119
126
|
const { ticker, trade, type } = event;
|
|
@@ -135,7 +142,7 @@ class UserHistoryReconstructor {
|
|
|
135
142
|
currentEventIdx++;
|
|
136
143
|
}
|
|
137
144
|
|
|
138
|
-
// Snapshot
|
|
145
|
+
// Snapshot "Held" State at EOD
|
|
139
146
|
for (const [posId, trade] of activePositions) {
|
|
140
147
|
const ticker = mappings.instrumentToTicker[trade.InstrumentID];
|
|
141
148
|
if (!ticker) continue;
|
|
@@ -143,16 +150,15 @@ class UserHistoryReconstructor {
|
|
|
143
150
|
if (!dayStats[ticker]) this.initTickerStats(dayStats, ticker);
|
|
144
151
|
|
|
145
152
|
const stats = dayStats[ticker];
|
|
146
|
-
stats.isHolder = 1;
|
|
153
|
+
stats.isHolder = 1;
|
|
147
154
|
stats.holdCount++;
|
|
148
155
|
stats.sumEntry += (trade.OpenRate || 0);
|
|
149
156
|
stats.sumLev += (trade.Leverage || 1);
|
|
150
157
|
}
|
|
151
158
|
|
|
152
|
-
// Finalize Averages
|
|
159
|
+
// Finalize Averages
|
|
153
160
|
for (const ticker in dayStats) {
|
|
154
161
|
const stats = dayStats[ticker];
|
|
155
|
-
|
|
156
162
|
if (stats.holdCount > 0) {
|
|
157
163
|
stats.avgEntry = stats.sumEntry / stats.holdCount;
|
|
158
164
|
stats.avgLeverage = stats.sumLev / stats.holdCount;
|
|
@@ -160,34 +166,27 @@ class UserHistoryReconstructor {
|
|
|
160
166
|
if (stats.didBuy > 0) {
|
|
161
167
|
stats.buyLeverage = stats.sumBuyLev / stats.didBuy;
|
|
162
168
|
}
|
|
163
|
-
|
|
164
|
-
// Cleanup temp sums
|
|
165
|
-
delete stats.sumEntry;
|
|
166
|
-
delete stats.sumLev;
|
|
167
|
-
delete stats.sumBuyLev;
|
|
168
|
-
delete stats.holdCount;
|
|
169
|
+
delete stats.sumEntry; delete stats.sumLev; delete stats.sumBuyLev; delete stats.holdCount;
|
|
169
170
|
}
|
|
170
171
|
|
|
171
|
-
//
|
|
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.
|
|
172
175
|
if (Object.keys(dayStats).length > 0) {
|
|
173
|
-
|
|
176
|
+
if (isEarliestRun) {
|
|
177
|
+
userTimeline[dateStr] = dayStats;
|
|
178
|
+
} else if (dateStr === currentExecutionDateStr) {
|
|
179
|
+
userTimeline[dateStr] = dayStats;
|
|
180
|
+
}
|
|
174
181
|
}
|
|
175
182
|
}
|
|
176
183
|
|
|
177
|
-
// 4. Output the full timeline for this user
|
|
178
|
-
// The ResultCommitter will handle splitting this into date buckets.
|
|
179
184
|
this.results[user.id] = userTimeline;
|
|
180
185
|
}
|
|
181
186
|
|
|
182
187
|
initTickerStats(dayStats, ticker) {
|
|
183
188
|
dayStats[ticker] = {
|
|
184
|
-
isHolder: 0,
|
|
185
|
-
didBuy: 0,
|
|
186
|
-
didSell: 0,
|
|
187
|
-
sumEntry: 0,
|
|
188
|
-
sumLev: 0,
|
|
189
|
-
holdCount: 0,
|
|
190
|
-
sumBuyLev: 0,
|
|
189
|
+
isHolder: 0, didBuy: 0, didSell: 0, sumEntry: 0, sumLev: 0, holdCount: 0, sumBuyLev: 0,
|
|
191
190
|
closeReasons: { "0": 0, "1": 0, "5": 0 }
|
|
192
191
|
};
|
|
193
192
|
}
|