aiden-shared-calculations-unified 1.0.106 → 1.0.108
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/asset-pnl-status.js +32 -40
- package/calculations/core/crowd-cost-basis.js +48 -15
- package/calculations/core/leverage-divergence.js +85 -47
- package/calculations/core/liquidation-cascade.js +43 -11
- package/calculations/core/user-history-reconstructor.js +128 -83
- package/calculations/gauss/daily-dna-filter.js +13 -24
- package/calculations/gem/cohort-skill-definition.js +13 -19
- package/calculations/pyro/squeeze-potential.js +2 -2
- package/package.json +1 -1
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
class AssetPnlStatus {
|
|
6
6
|
constructor() {
|
|
7
|
-
this.
|
|
7
|
+
// [FIX 1] Use 'this.results' directly.
|
|
8
|
+
// The StandardExecutor monitors this property to perform Batch Flushing.
|
|
9
|
+
this.results = {};
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
static getMetadata() {
|
|
@@ -22,29 +24,26 @@ class AssetPnlStatus {
|
|
|
22
24
|
static getSchema() {
|
|
23
25
|
return {
|
|
24
26
|
"type": "object",
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
+
// [FIX 2] Flattened Schema (removed "by_user" wrapper)
|
|
28
|
+
"patternProperties": {
|
|
29
|
+
"^[0-9]+$": {
|
|
27
30
|
"type": "object",
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"losers": { "type": "array", "items": { "type": "string" } }
|
|
34
|
-
},
|
|
35
|
-
"required": ["winners", "losers"]
|
|
36
|
-
}
|
|
37
|
-
}
|
|
31
|
+
"properties": {
|
|
32
|
+
"winners": { "type": "array", "items": { "type": "string" } },
|
|
33
|
+
"losers": { "type": "array", "items": { "type": "string" } }
|
|
34
|
+
},
|
|
35
|
+
"required": ["winners", "losers"]
|
|
38
36
|
}
|
|
39
37
|
}
|
|
40
38
|
};
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
_initUser(userId) {
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
// [FIX 3] Direct access to this.results
|
|
43
|
+
if (!this.results[userId]) {
|
|
44
|
+
this.results[userId] = { winners: [], losers: [] };
|
|
46
45
|
}
|
|
47
|
-
return this.
|
|
46
|
+
return this.results[userId];
|
|
48
47
|
}
|
|
49
48
|
|
|
50
49
|
process(context) {
|
|
@@ -55,7 +54,9 @@ class AssetPnlStatus {
|
|
|
55
54
|
const positions = extract.getPositions(user.portfolio.today, user.type);
|
|
56
55
|
if (!positions || positions.length === 0) return;
|
|
57
56
|
|
|
58
|
-
|
|
57
|
+
// Use temporary sets for processing to keep logic clean, then convert to array for storage
|
|
58
|
+
const winners = new Set();
|
|
59
|
+
const losers = new Set();
|
|
59
60
|
|
|
60
61
|
for (const pos of positions) {
|
|
61
62
|
const instId = extract.getInstrumentId(pos);
|
|
@@ -68,35 +69,26 @@ class AssetPnlStatus {
|
|
|
68
69
|
const sector = mappings.instrumentToSector[instId];
|
|
69
70
|
const isWinner = pnl > 0;
|
|
70
71
|
|
|
71
|
-
if (ticker
|
|
72
|
-
|
|
73
|
-
if (ticker) {
|
|
74
|
-
if (isWinner) stats.winners.add(ticker); else stats.losers.add(ticker);
|
|
75
|
-
}
|
|
76
|
-
if (sector) {
|
|
77
|
-
if (isWinner) stats.winners.add(sector); else stats.losers.add(sector);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
72
|
+
if (ticker) { isWinner ? winners.add(ticker) : losers.add(ticker); }
|
|
73
|
+
if (sector) { isWinner ? winners.add(sector) : losers.add(sector); }
|
|
80
74
|
}
|
|
81
|
-
}
|
|
82
75
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
byUser[userId] = {
|
|
89
|
-
winners: Array.from(stats.winners),
|
|
90
|
-
losers: Array.from(stats.losers)
|
|
91
|
-
};
|
|
92
|
-
}
|
|
76
|
+
if (winners.size > 0 || losers.size > 0) {
|
|
77
|
+
this.results[userId] = {
|
|
78
|
+
winners: Array.from(winners),
|
|
79
|
+
losers: Array.from(losers)
|
|
80
|
+
};
|
|
93
81
|
}
|
|
82
|
+
}
|
|
94
83
|
|
|
95
|
-
|
|
96
|
-
|
|
84
|
+
async getResult() {
|
|
85
|
+
// [FIX 4] Return 'this.results' directly.
|
|
86
|
+
// This is a FLAT object { "user1": {...}, "user2": {...} }
|
|
87
|
+
// The ResultCommitter can now shard this into 50-user chunks if needed.
|
|
88
|
+
return this.results;
|
|
97
89
|
}
|
|
98
90
|
|
|
99
|
-
reset
|
|
91
|
+
// [FIX 5] No manual reset needed, StandardExecutor does it.
|
|
100
92
|
}
|
|
101
93
|
|
|
102
94
|
module.exports = AssetPnlStatus;
|
|
@@ -7,7 +7,7 @@ class CrowdCostBasis {
|
|
|
7
7
|
type: 'meta',
|
|
8
8
|
category: 'History Reconstruction',
|
|
9
9
|
userType: 'n/a',
|
|
10
|
-
isHistorical: false,
|
|
10
|
+
isHistorical: false, // Self-contained history generation
|
|
11
11
|
rootDataDependencies: ['price']
|
|
12
12
|
};
|
|
13
13
|
}
|
|
@@ -15,7 +15,7 @@ class CrowdCostBasis {
|
|
|
15
15
|
static getDependencies() { return ['user-history-reconstructor']; }
|
|
16
16
|
|
|
17
17
|
static getSchema() {
|
|
18
|
-
const
|
|
18
|
+
const dailySchema = {
|
|
19
19
|
"type": "object",
|
|
20
20
|
"properties": {
|
|
21
21
|
"avgEntry": { "type": "number", "description": "Global average entry price for all holders." },
|
|
@@ -25,20 +25,48 @@ class CrowdCostBasis {
|
|
|
25
25
|
},
|
|
26
26
|
"required": ["avgEntry", "holderCount", "profitabilityPct", "state"]
|
|
27
27
|
};
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
|
|
29
|
+
// Output: Date -> Ticker -> Schema
|
|
30
|
+
return {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"patternProperties": {
|
|
33
|
+
"^\\d{4}-\\d{2}-\\d{2}$": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"patternProperties": { "^.*$": dailySchema }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
30
39
|
}
|
|
31
40
|
|
|
32
41
|
async process(context) {
|
|
33
42
|
const { computed, prices, math } = context;
|
|
34
|
-
const
|
|
35
|
-
if (!
|
|
43
|
+
const historyData = computed['user-history-reconstructor'];
|
|
44
|
+
if (!historyData) return;
|
|
45
|
+
|
|
46
|
+
// Detect Structure: Date-First (Time Machine) or User-First (Legacy)
|
|
47
|
+
// With the new StandardExecutor refactor, it should be Date -> User -> Ticker
|
|
48
|
+
const keys = Object.keys(historyData);
|
|
49
|
+
const isDateKeyed = keys.length > 0 && /^\d{4}-\d{2}-\d{2}$/.test(keys[0]);
|
|
50
|
+
|
|
51
|
+
if (isDateKeyed) {
|
|
52
|
+
// Process All Dates in the Map
|
|
53
|
+
for (const dateStr of keys) {
|
|
54
|
+
this.results[dateStr] = this.processSingleDate(dateStr, historyData[dateStr], prices, math);
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
// Fallback for single-date/legacy context
|
|
58
|
+
const dateStr = context.date.today;
|
|
59
|
+
this.results[dateStr] = this.processSingleDate(dateStr, historyData, prices, math);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
36
62
|
|
|
63
|
+
processSingleDate(dateStr, usersMap, prices, math) {
|
|
37
64
|
const aggregator = {};
|
|
65
|
+
const dailyResults = {};
|
|
38
66
|
|
|
39
|
-
// 1. Iterate Users
|
|
40
|
-
for (const userId in
|
|
41
|
-
const userPortfolio =
|
|
67
|
+
// 1. Iterate Users for this Date
|
|
68
|
+
for (const userId in usersMap) {
|
|
69
|
+
const userPortfolio = usersMap[userId];
|
|
42
70
|
|
|
43
71
|
for (const ticker in userPortfolio) {
|
|
44
72
|
const stats = userPortfolio[ticker];
|
|
@@ -54,24 +82,29 @@ class CrowdCostBasis {
|
|
|
54
82
|
// 2. Compute Global Cost Basis
|
|
55
83
|
for (const ticker in aggregator) {
|
|
56
84
|
const data = aggregator[ticker];
|
|
57
|
-
if (data.count <
|
|
85
|
+
if (data.count < 3) continue; // Noise filter
|
|
58
86
|
|
|
59
87
|
const globalAvgEntry = data.sumEntry / data.count;
|
|
60
88
|
|
|
61
|
-
// Get Price for
|
|
89
|
+
// Get Price for this specific date
|
|
90
|
+
// Note: prices.history might be sharded, assuming ContextFactory loaded relevant shards
|
|
62
91
|
const priceHistory = math.priceExtractor.getHistory(prices, ticker);
|
|
63
|
-
|
|
64
|
-
|
|
92
|
+
|
|
93
|
+
// Find price closest to this date
|
|
94
|
+
// Optimization: In a real "Time Machine" run, priceExtractor should probably be a map lookup
|
|
95
|
+
const dayPriceObj = priceHistory.find(p => p.date === dateStr);
|
|
96
|
+
const currentPrice = dayPriceObj ? dayPriceObj.price : globalAvgEntry;
|
|
65
97
|
|
|
66
|
-
const diffPct = ((currentPrice - globalAvgEntry) / globalAvgEntry) * 100;
|
|
98
|
+
const diffPct = globalAvgEntry > 0 ? ((currentPrice - globalAvgEntry) / globalAvgEntry) * 100 : 0;
|
|
67
99
|
|
|
68
|
-
|
|
100
|
+
dailyResults[ticker] = {
|
|
69
101
|
avgEntry: globalAvgEntry,
|
|
70
102
|
holderCount: data.count,
|
|
71
103
|
profitabilityPct: diffPct,
|
|
72
104
|
state: diffPct > 0 ? 'PROFIT_SUPPORT' : 'LOSS_RESISTANCE'
|
|
73
105
|
};
|
|
74
106
|
}
|
|
107
|
+
return dailyResults;
|
|
75
108
|
}
|
|
76
109
|
|
|
77
110
|
async getResult() { return this.results; }
|
|
@@ -7,81 +7,119 @@ class LeverageDivergence {
|
|
|
7
7
|
type: 'meta',
|
|
8
8
|
category: 'History Reconstruction',
|
|
9
9
|
userType: 'n/a',
|
|
10
|
-
isHistorical:
|
|
11
|
-
rootDataDependencies: []
|
|
10
|
+
isHistorical: false, // We handle history internally now
|
|
11
|
+
rootDataDependencies: ['price'] // Needed for Smart/Dumb PnL calc
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
static getDependencies() { return ['user-history-reconstructor']; }
|
|
16
16
|
|
|
17
17
|
static getSchema() {
|
|
18
|
-
const
|
|
18
|
+
const dailySchema = {
|
|
19
19
|
"type": "object",
|
|
20
20
|
"properties": {
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"signal": { "type": "string", "description": "Divergence signal: NEUTRAL, SMART_ACCUMULATION, SPECULATIVE_PUMP, SMART_EXIT." }
|
|
21
|
+
"smartAccumulation": { "type": "number", "description": "Buy volume from profitable users." },
|
|
22
|
+
"dumbAccumulation": { "type": "number", "description": "Buy volume from unprofitable/high-lev users." },
|
|
23
|
+
"netFlow": { "type": "number", "description": "Net buy/sell activity." },
|
|
24
|
+
"signal": { "type": "string", "description": "Divergence signal: NEUTRAL, SMART_ACCUMULATION, SPECULATIVE_PUMP, BROAD_EXIT." }
|
|
26
25
|
},
|
|
27
|
-
"required": ["
|
|
26
|
+
"required": ["smartAccumulation", "dumbAccumulation", "netFlow", "signal"]
|
|
27
|
+
};
|
|
28
|
+
// Output: Date -> Ticker -> Schema
|
|
29
|
+
return {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"patternProperties": {
|
|
32
|
+
"^\\d{4}-\\d{2}-\\d{2}$": {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"patternProperties": { "^.*$": dailySchema }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
28
37
|
};
|
|
29
|
-
return { "type": "object", "patternProperties": { "^.*$": schema } };
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
async process(context) {
|
|
33
|
-
const { computed,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
const { computed, prices, math } = context;
|
|
42
|
+
const historyData = computed['user-history-reconstructor'];
|
|
43
|
+
if (!historyData) return;
|
|
44
|
+
|
|
45
|
+
const keys = Object.keys(historyData);
|
|
46
|
+
const isDateKeyed = keys.length > 0 && /^\d{4}-\d{2}-\d{2}$/.test(keys[0]);
|
|
47
|
+
const dateList = isDateKeyed ? keys.sort() : [context.date.today];
|
|
48
|
+
|
|
49
|
+
// Process sequentially to allow day-over-day comparison if needed (though we use raw flows here)
|
|
50
|
+
for (const dateStr of dateList) {
|
|
51
|
+
const usersMap = isDateKeyed ? historyData[dateStr] : historyData;
|
|
52
|
+
this.results[dateStr] = this.processSingleDate(dateStr, usersMap, prices, math);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
37
55
|
|
|
38
|
-
|
|
56
|
+
processSingleDate(dateStr, usersMap, prices, math) {
|
|
57
|
+
const tickerAgg = {}; // Ticker -> { smartBuys, dumbBuys, sells }
|
|
39
58
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const userPortfolio = currentReconstruction[userId];
|
|
59
|
+
for (const userId in usersMap) {
|
|
60
|
+
const userPortfolio = usersMap[userId];
|
|
43
61
|
|
|
44
62
|
for (const ticker in userPortfolio) {
|
|
45
63
|
const stats = userPortfolio[ticker];
|
|
64
|
+
|
|
65
|
+
// We only care about Activity (Buys/Sells)
|
|
66
|
+
if (stats.didBuy === 0 && stats.didSell === 0) continue;
|
|
67
|
+
|
|
68
|
+
if (!tickerAgg[ticker]) tickerAgg[ticker] = { smartBuys: 0, dumbBuys: 0, sells: 0 };
|
|
69
|
+
|
|
70
|
+
// 1. Classify User "Smartness" for this Ticker/Date
|
|
71
|
+
// Heuristic: Are they profitable on their holdings relative to TODAY's price?
|
|
72
|
+
const priceHistory = math.priceExtractor.getHistory(prices, ticker);
|
|
73
|
+
const dayPriceObj = priceHistory.find(p => p.date === dateStr);
|
|
74
|
+
const currentPrice = dayPriceObj ? dayPriceObj.price : stats.avgEntry;
|
|
75
|
+
|
|
76
|
+
let isSmart = false;
|
|
77
|
+
if (stats.avgEntry > 0 && currentPrice > 0) {
|
|
78
|
+
const pnlPct = ((currentPrice - stats.avgEntry) / stats.avgEntry) * 100;
|
|
79
|
+
// Smart = Profitable (>2%) OR (Small Loss > -2% AND Low Leverage < 2x)
|
|
80
|
+
// Dumb = Unprofitable (< -5%) OR High Leverage (> 5x)
|
|
81
|
+
if (pnlPct > 2) isSmart = true;
|
|
82
|
+
else if (pnlPct > -2 && stats.avgLeverage < 2) isSmart = true;
|
|
83
|
+
}
|
|
46
84
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
85
|
+
// 2. Accumulate Flows
|
|
86
|
+
if (stats.didBuy > 0) {
|
|
87
|
+
if (isSmart) tickerAgg[ticker].smartBuys += stats.didBuy;
|
|
88
|
+
else tickerAgg[ticker].dumbBuys += stats.didBuy;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (stats.didSell > 0) {
|
|
92
|
+
tickerAgg[ticker].sells += stats.didSell;
|
|
55
93
|
}
|
|
56
94
|
}
|
|
57
95
|
}
|
|
58
96
|
|
|
59
|
-
|
|
60
|
-
for (const ticker in
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
if (!prev) {
|
|
65
|
-
this.results[ticker] = { ...curr, levDelta: 0, spotDelta: 0, signal: 'NEW' };
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
97
|
+
const dailyResult = {};
|
|
98
|
+
for (const ticker in tickerAgg) {
|
|
99
|
+
const { smartBuys, dumbBuys, sells } = tickerAgg[ticker];
|
|
100
|
+
const netFlow = (smartBuys + dumbBuys) - sells;
|
|
101
|
+
const totalActivity = smartBuys + dumbBuys + sells;
|
|
68
102
|
|
|
69
|
-
|
|
70
|
-
const spotDelta = curr.spotHolders - prev.spotHolders;
|
|
103
|
+
if (totalActivity < 3) continue;
|
|
71
104
|
|
|
72
105
|
let signal = 'NEUTRAL';
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
106
|
+
|
|
107
|
+
if (netFlow > 0) {
|
|
108
|
+
if (smartBuys > dumbBuys) signal = 'SMART_ACCUMULATION';
|
|
109
|
+
else signal = 'SPECULATIVE_PUMP'; // Mostly dumb/lev money buying
|
|
110
|
+
} else if (netFlow < 0) {
|
|
111
|
+
signal = 'BROAD_EXIT';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
dailyResult[ticker] = {
|
|
115
|
+
smartAccumulation: smartBuys,
|
|
116
|
+
dumbAccumulation: dumbBuys,
|
|
117
|
+
netFlow: netFlow,
|
|
118
|
+
signal: signal
|
|
83
119
|
};
|
|
84
120
|
}
|
|
121
|
+
|
|
122
|
+
return dailyResult;
|
|
85
123
|
}
|
|
86
124
|
|
|
87
125
|
async getResult() { return this.results; }
|
|
@@ -15,57 +15,89 @@ class LiquidationCascade {
|
|
|
15
15
|
static getDependencies() { return ['user-history-reconstructor']; }
|
|
16
16
|
|
|
17
17
|
static getSchema() {
|
|
18
|
-
const
|
|
18
|
+
const dailySchema = {
|
|
19
19
|
"type": "object",
|
|
20
20
|
"properties": {
|
|
21
21
|
"totalClosures": { "type": "number", "description": "Total number of positions closed today." },
|
|
22
22
|
"forcedClosures": { "type": "number", "description": "Number of positions closed via Stop Loss (Reason 1)." },
|
|
23
|
+
"avgClosedLeverage": { "type": "number", "description": "Average leverage of the closed positions." },
|
|
23
24
|
"painIndex": { "type": "number", "description": "Ratio of forced closures to total closures (0.0 - 1.0)." },
|
|
24
|
-
"isFlushEvent": { "type": "boolean", "description": "True if painIndex > 0.3." }
|
|
25
|
+
"isFlushEvent": { "type": "boolean", "description": "True if painIndex > 0.3 AND avgClosedLeverage > 3." }
|
|
25
26
|
},
|
|
26
27
|
"required": ["totalClosures", "forcedClosures", "painIndex", "isFlushEvent"]
|
|
27
28
|
};
|
|
28
|
-
|
|
29
|
+
// Output: Date -> Ticker -> Schema
|
|
30
|
+
return {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"patternProperties": {
|
|
33
|
+
"^\\d{4}-\\d{2}-\\d{2}$": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"patternProperties": { "^.*$": dailySchema }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
async process(context) {
|
|
32
42
|
const { computed } = context;
|
|
33
|
-
const
|
|
34
|
-
if (!
|
|
43
|
+
const historyData = computed['user-history-reconstructor'];
|
|
44
|
+
if (!historyData) return;
|
|
45
|
+
|
|
46
|
+
const keys = Object.keys(historyData);
|
|
47
|
+
const isDateKeyed = keys.length > 0 && /^\d{4}-\d{2}-\d{2}$/.test(keys[0]);
|
|
35
48
|
|
|
49
|
+
if (isDateKeyed) {
|
|
50
|
+
for (const dateStr of keys) {
|
|
51
|
+
this.results[dateStr] = this.processSingleDate(historyData[dateStr]);
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
this.results[context.date.today] = this.processSingleDate(historyData);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
processSingleDate(usersMap) {
|
|
36
59
|
const aggregator = {};
|
|
60
|
+
const dailyResults = {};
|
|
37
61
|
|
|
38
|
-
for (const userId in
|
|
39
|
-
const userPortfolio =
|
|
62
|
+
for (const userId in usersMap) {
|
|
63
|
+
const userPortfolio = usersMap[userId];
|
|
40
64
|
|
|
41
65
|
for (const ticker in userPortfolio) {
|
|
42
66
|
const stats = userPortfolio[ticker];
|
|
43
67
|
|
|
44
68
|
if (stats.didSell > 0) {
|
|
45
|
-
if (!aggregator[ticker]) aggregator[ticker] = { totalClosed: 0, forced: 0 };
|
|
69
|
+
if (!aggregator[ticker]) aggregator[ticker] = { totalClosed: 0, forced: 0, sumLev: 0 };
|
|
46
70
|
|
|
47
71
|
aggregator[ticker].totalClosed += stats.didSell;
|
|
48
72
|
|
|
73
|
+
// Reason 1 is usually Stop Loss / Liquidation in cTrader/MetaTrader schemas
|
|
49
74
|
if (stats.closeReasons && stats.closeReasons["1"]) {
|
|
50
75
|
aggregator[ticker].forced += stats.closeReasons["1"];
|
|
51
76
|
}
|
|
77
|
+
|
|
78
|
+
// We use avgLeverage of the holding as a proxy for the closed leverage
|
|
79
|
+
aggregator[ticker].sumLev += (stats.avgLeverage * stats.didSell);
|
|
52
80
|
}
|
|
53
81
|
}
|
|
54
82
|
}
|
|
55
83
|
|
|
56
84
|
for (const ticker in aggregator) {
|
|
57
85
|
const data = aggregator[ticker];
|
|
58
|
-
if (data.totalClosed <
|
|
86
|
+
if (data.totalClosed < 3) continue;
|
|
59
87
|
|
|
60
88
|
const forcedRatio = data.forced / data.totalClosed;
|
|
89
|
+
const avgLev = data.sumLev / data.totalClosed;
|
|
61
90
|
|
|
62
|
-
|
|
91
|
+
dailyResults[ticker] = {
|
|
63
92
|
totalClosures: data.totalClosed,
|
|
64
93
|
forcedClosures: data.forced,
|
|
94
|
+
avgClosedLeverage: avgLev,
|
|
65
95
|
painIndex: forcedRatio,
|
|
66
|
-
|
|
96
|
+
// Flush = High Pain AND Significant Leverage involved
|
|
97
|
+
isFlushEvent: forcedRatio > 0.3 && avgLev > 3
|
|
67
98
|
};
|
|
68
99
|
}
|
|
100
|
+
return dailyResults;
|
|
69
101
|
}
|
|
70
102
|
|
|
71
103
|
async getResult() { return this.results; }
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
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 (Holders, Buys, Sells).
|
|
4
|
+
*/
|
|
5
|
+
|
|
1
6
|
class UserHistoryReconstructor {
|
|
2
7
|
constructor() {
|
|
3
8
|
this.results = {};
|
|
@@ -9,7 +14,8 @@ class UserHistoryReconstructor {
|
|
|
9
14
|
type: 'standard',
|
|
10
15
|
category: 'History Reconstruction',
|
|
11
16
|
userType: 'all',
|
|
12
|
-
|
|
17
|
+
// False because we generate history internally from the snapshot
|
|
18
|
+
isHistorical: false,
|
|
13
19
|
rootDataDependencies: ['history']
|
|
14
20
|
};
|
|
15
21
|
}
|
|
@@ -17,134 +23,173 @@ class UserHistoryReconstructor {
|
|
|
17
23
|
static getDependencies() { return []; }
|
|
18
24
|
|
|
19
25
|
static getSchema() {
|
|
20
|
-
// Schema for a single Ticker's stats
|
|
21
|
-
const
|
|
26
|
+
// Schema for a single Ticker's stats for a single Day
|
|
27
|
+
const tickerDailyStats = {
|
|
22
28
|
"type": "object",
|
|
23
29
|
"properties": {
|
|
24
30
|
"isHolder": { "type": "number", "description": "1 if the user holds the asset at EOD, 0 otherwise." },
|
|
25
|
-
"didBuy": { "type": "number", "description": "Count of buy trades executed
|
|
26
|
-
"didSell": { "type": "number", "description": "Count of sell trades executed
|
|
31
|
+
"didBuy": { "type": "number", "description": "Count of buy trades executed this day." },
|
|
32
|
+
"didSell": { "type": "number", "description": "Count of sell trades executed this day." },
|
|
27
33
|
"avgEntry": { "type": "number", "description": "Average entry price of held positions." },
|
|
28
34
|
"avgLeverage": { "type": "number", "description": "Average leverage of held positions." },
|
|
29
|
-
"buyLeverage": { "type": "number", "description": "Average leverage of new buys executed
|
|
35
|
+
"buyLeverage": { "type": "number", "description": "Average leverage of new buys executed this day." },
|
|
30
36
|
"closeReasons": {
|
|
31
37
|
"type": "object",
|
|
32
38
|
"description": "Map of close reason codes to counts (0=Manual, 1=Stop/Liq, 5=TP)."
|
|
33
39
|
}
|
|
34
|
-
}
|
|
35
|
-
"required": ["isHolder", "didBuy", "didSell", "avgEntry", "avgLeverage"]
|
|
40
|
+
}
|
|
36
41
|
};
|
|
37
42
|
|
|
38
|
-
//
|
|
43
|
+
// Output is now: Date -> Ticker -> Stats
|
|
39
44
|
return {
|
|
40
45
|
"type": "object",
|
|
41
46
|
"patternProperties": {
|
|
42
|
-
|
|
47
|
+
// Key is YYYY-MM-DD
|
|
48
|
+
"^\\d{4}-\\d{2}-\\d{2}$": {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"patternProperties": {
|
|
51
|
+
// Key is Ticker
|
|
52
|
+
"^.*$": tickerDailyStats
|
|
53
|
+
}
|
|
54
|
+
}
|
|
43
55
|
}
|
|
44
56
|
};
|
|
45
57
|
}
|
|
46
58
|
|
|
47
59
|
async process(context) {
|
|
48
|
-
const { user, math, mappings
|
|
60
|
+
const { user, math, mappings } = context;
|
|
49
61
|
|
|
50
|
-
// 1. Get History (V2 Format
|
|
62
|
+
// 1. Get History (Granular V2 Format)
|
|
51
63
|
const history = math.history.getDailyHistory(user);
|
|
52
64
|
const allTrades = history?.PublicHistoryPositions || [];
|
|
53
65
|
|
|
54
66
|
if (allTrades.length === 0) return;
|
|
55
67
|
|
|
56
|
-
// 2.
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
this.results[user.id] = {};
|
|
61
|
-
const userState = this.results[user.id];
|
|
62
|
-
|
|
68
|
+
// 2. Identify all relevant tickers and dates
|
|
69
|
+
const events = []; // { time, type, trade, ticker }
|
|
70
|
+
const tickerSet = new Set();
|
|
71
|
+
|
|
63
72
|
for (const trade of allTrades) {
|
|
64
73
|
const instId = trade.InstrumentID;
|
|
65
74
|
const ticker = mappings.instrumentToTicker[instId];
|
|
66
75
|
if (!ticker) continue;
|
|
76
|
+
tickerSet.add(ticker);
|
|
67
77
|
|
|
68
|
-
// Parse Dates
|
|
69
78
|
const openTime = new Date(trade.OpenDateTime).getTime();
|
|
70
79
|
const closeTime = trade.CloseDateTime ? new Date(trade.CloseDateTime).getTime() : null;
|
|
71
80
|
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
81
|
+
// Add OPEN event
|
|
82
|
+
events.push({ time: openTime, type: 'OPEN', trade, ticker });
|
|
83
|
+
|
|
84
|
+
// Add CLOSE event (if closed)
|
|
85
|
+
if (closeTime) { events.push({ time: closeTime, type: 'CLOSE', trade, ticker }); }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Sort events by time
|
|
89
|
+
events.sort((a, b) => a.time - b.time);
|
|
90
|
+
|
|
91
|
+
if (events.length === 0) return;
|
|
92
|
+
|
|
93
|
+
// 3. Scan-Line Execution (Replay History)
|
|
94
|
+
const userTimeline = {}; // { "2024-01-01": { "AAPL": {...} } }
|
|
95
|
+
|
|
96
|
+
// Tracking State
|
|
97
|
+
const activePositions = new Map(); // Map<PositionID, Trade>
|
|
98
|
+
|
|
99
|
+
// Determine Start and End dates for the loop
|
|
100
|
+
const firstEventTime = events[0].time;
|
|
101
|
+
const lastEventTime = events[events.length - 1].time;
|
|
102
|
+
|
|
103
|
+
const startDate = new Date(firstEventTime); startDate.setUTCHours(0,0,0,0);
|
|
104
|
+
const endDate = new Date(lastEventTime); endDate.setUTCHours(0,0,0,0);
|
|
105
|
+
|
|
106
|
+
let currentEventIdx = 0;
|
|
107
|
+
const oneDayMs = 86400000;
|
|
108
|
+
|
|
109
|
+
// Iterate day by day from first trade to last trade
|
|
110
|
+
for (let d = startDate.getTime(); d <= endDate.getTime(); d += oneDayMs) {
|
|
111
|
+
const dateStr = new Date(d).toISOString().slice(0, 10);
|
|
112
|
+
const dayEnd = d + oneDayMs - 1;
|
|
113
|
+
|
|
114
|
+
const dayStats = {}; // Ticker -> Stats
|
|
115
|
+
|
|
116
|
+
// Process all events that happened TODAY (before EOD)
|
|
117
|
+
while (currentEventIdx < events.length && events[currentEventIdx].time <= dayEnd) {
|
|
118
|
+
const event = events[currentEventIdx];
|
|
119
|
+
const { ticker, trade, type } = event;
|
|
120
|
+
|
|
121
|
+
if (!dayStats[ticker]) this.initTickerStats(dayStats, ticker);
|
|
122
|
+
|
|
123
|
+
if (type === 'OPEN') {
|
|
124
|
+
activePositions.set(trade.PositionID, trade);
|
|
125
|
+
dayStats[ticker].didBuy++;
|
|
126
|
+
dayStats[ticker].sumBuyLev += (trade.Leverage || 1);
|
|
127
|
+
} else if (type === 'CLOSE') {
|
|
128
|
+
activePositions.delete(trade.PositionID);
|
|
129
|
+
dayStats[ticker].didSell++;
|
|
130
|
+
|
|
131
|
+
const reason = String(trade.CloseReason || 0);
|
|
132
|
+
if (dayStats[ticker].closeReasons[reason] === undefined) dayStats[ticker].closeReasons[reason] = 0;
|
|
133
|
+
dayStats[ticker].closeReasons[reason]++;
|
|
134
|
+
}
|
|
135
|
+
currentEventIdx++;
|
|
101
136
|
}
|
|
102
|
-
const stats = userState[ticker];
|
|
103
137
|
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
138
|
+
// Snapshot the "Held" State at EOD
|
|
139
|
+
for (const [posId, trade] of activePositions) {
|
|
140
|
+
const ticker = mappings.instrumentToTicker[trade.InstrumentID];
|
|
141
|
+
if (!ticker) continue;
|
|
142
|
+
|
|
143
|
+
if (!dayStats[ticker]) this.initTickerStats(dayStats, ticker);
|
|
144
|
+
|
|
145
|
+
const stats = dayStats[ticker];
|
|
146
|
+
stats.isHolder = 1; // User is a holder today
|
|
107
147
|
stats.holdCount++;
|
|
108
148
|
stats.sumEntry += (trade.OpenRate || 0);
|
|
109
149
|
stats.sumLev += (trade.Leverage || 1);
|
|
110
150
|
}
|
|
111
151
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
stats
|
|
115
|
-
|
|
152
|
+
// Finalize Averages for this Day
|
|
153
|
+
for (const ticker in dayStats) {
|
|
154
|
+
const stats = dayStats[ticker];
|
|
155
|
+
|
|
156
|
+
if (stats.holdCount > 0) {
|
|
157
|
+
stats.avgEntry = stats.sumEntry / stats.holdCount;
|
|
158
|
+
stats.avgLeverage = stats.sumLev / stats.holdCount;
|
|
159
|
+
}
|
|
160
|
+
if (stats.didBuy > 0) {
|
|
161
|
+
stats.buyLeverage = stats.sumBuyLev / stats.didBuy;
|
|
162
|
+
}
|
|
116
163
|
|
|
117
|
-
|
|
118
|
-
stats.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
stats.
|
|
164
|
+
// Cleanup temp sums
|
|
165
|
+
delete stats.sumEntry;
|
|
166
|
+
delete stats.sumLev;
|
|
167
|
+
delete stats.sumBuyLev;
|
|
168
|
+
delete stats.holdCount;
|
|
122
169
|
}
|
|
123
|
-
}
|
|
124
170
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (stats.holdCount > 0) {
|
|
130
|
-
stats.avgEntry = stats.sumEntry / stats.holdCount;
|
|
131
|
-
stats.avgLeverage = stats.sumLev / stats.holdCount;
|
|
132
|
-
} else {
|
|
133
|
-
stats.avgEntry = 0;
|
|
134
|
-
stats.avgLeverage = 0;
|
|
171
|
+
// Store in timeline if any activity exists for this day
|
|
172
|
+
if (Object.keys(dayStats).length > 0) {
|
|
173
|
+
userTimeline[dateStr] = dayStats;
|
|
135
174
|
}
|
|
175
|
+
}
|
|
136
176
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
177
|
+
// 4. Output the full timeline for this user
|
|
178
|
+
// The ResultCommitter will handle splitting this into date buckets.
|
|
179
|
+
this.results[user.id] = userTimeline;
|
|
180
|
+
}
|
|
142
181
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
182
|
+
initTickerStats(dayStats, ticker) {
|
|
183
|
+
dayStats[ticker] = {
|
|
184
|
+
isHolder: 0,
|
|
185
|
+
didBuy: 0,
|
|
186
|
+
didSell: 0,
|
|
187
|
+
sumEntry: 0,
|
|
188
|
+
sumLev: 0,
|
|
189
|
+
holdCount: 0,
|
|
190
|
+
sumBuyLev: 0,
|
|
191
|
+
closeReasons: { "0": 0, "1": 0, "5": 0 }
|
|
192
|
+
};
|
|
148
193
|
}
|
|
149
194
|
|
|
150
195
|
async getResult() { return this.results; }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview GAUSS Product Line (Pass 1)
|
|
3
|
-
* REFACTORED: Uses
|
|
3
|
+
* REFACTORED: Uses the centralized SmartMoneyScorer for consistent classification.
|
|
4
4
|
*/
|
|
5
5
|
class DailyDnaFilter {
|
|
6
6
|
constructor() {
|
|
@@ -10,7 +10,7 @@ class DailyDnaFilter {
|
|
|
10
10
|
static getMetadata() {
|
|
11
11
|
return {
|
|
12
12
|
type: 'standard',
|
|
13
|
-
rootDataDependencies: ['history'],
|
|
13
|
+
rootDataDependencies: ['history', 'portfolio', 'price'], // Added portfolio/price for hybrid scoring
|
|
14
14
|
isHistorical: false,
|
|
15
15
|
userType: 'all',
|
|
16
16
|
category: 'gauss'
|
|
@@ -33,30 +33,17 @@ class DailyDnaFilter {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
process(context) {
|
|
36
|
-
|
|
37
|
-
const {
|
|
38
|
-
|
|
39
|
-
// 1. Get strict daily history data
|
|
40
|
-
const historyDoc = history.getDailyHistory(user);
|
|
41
|
-
|
|
42
|
-
// 2. Get strict summary object using the tool
|
|
43
|
-
const summary = history.getSummary(historyDoc);
|
|
44
|
-
|
|
45
|
-
// Validation
|
|
46
|
-
if (!summary || summary.totalTrades < 20) return;
|
|
36
|
+
// Use the centralized Intelligence Engine injected via Context
|
|
37
|
+
const { SmartMoneyScorer } = context.math;
|
|
47
38
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
// CORRECTED LINE: uses summary.avgProfitPct
|
|
53
|
-
const avgWin = summary.avgProfitPct;
|
|
54
|
-
const avgLoss = Math.abs(summary.avgLossPct);
|
|
55
|
-
|
|
56
|
-
const lt_skill = (winRate * avgWin) - (lossRate * avgLoss);
|
|
39
|
+
if (!SmartMoneyScorer) return;
|
|
40
|
+
|
|
41
|
+
const result = SmartMoneyScorer.scoreHybrid(context);
|
|
42
|
+
const score = result.totalScore;
|
|
57
43
|
|
|
58
|
-
|
|
59
|
-
|
|
44
|
+
// Filter out users with insufficient data (Score 0 or Neutral with no method)
|
|
45
|
+
if (score > 0) {
|
|
46
|
+
this.allUserSkills.push([context.user.id, score]);
|
|
60
47
|
}
|
|
61
48
|
}
|
|
62
49
|
|
|
@@ -64,8 +51,10 @@ class DailyDnaFilter {
|
|
|
64
51
|
const totalUsers = this.allUserSkills.length;
|
|
65
52
|
if (totalUsers === 0) return { smart_cohort_ids: [], dumb_cohort_ids: [], total_users_analyzed: 0, cohort_size: 0 };
|
|
66
53
|
|
|
54
|
+
// Sort Descending (Highest Score First)
|
|
67
55
|
this.allUserSkills.sort((a, b) => b[1] - a[1]);
|
|
68
56
|
|
|
57
|
+
// Top 20% Smart, Bottom 20% Dumb
|
|
69
58
|
const cohortSize = Math.floor(totalUsers * 0.20);
|
|
70
59
|
if (cohortSize === 0) return { smart_cohort_ids: [], dumb_cohort_ids: [], total_users_analyzed: totalUsers, cohort_size: 0 };
|
|
71
60
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview GEM Product Line (Pass 1)
|
|
3
|
-
* REFACTORED:
|
|
3
|
+
* REFACTORED: Uses SmartMoneyScorer for advanced hybrid classification.
|
|
4
4
|
*/
|
|
5
5
|
class CohortSkillDefinition {
|
|
6
6
|
constructor() { this.userScores = new Map(); }
|
|
@@ -21,7 +21,8 @@ class CohortSkillDefinition {
|
|
|
21
21
|
static getMetadata() {
|
|
22
22
|
return {
|
|
23
23
|
type: 'standard',
|
|
24
|
-
|
|
24
|
+
// Updated dependencies to support hybrid scoring
|
|
25
|
+
rootDataDependencies: ['history', 'portfolio', 'price'],
|
|
25
26
|
isHistorical: false,
|
|
26
27
|
userType: 'all',
|
|
27
28
|
category: 'gem'
|
|
@@ -31,26 +32,19 @@ class CohortSkillDefinition {
|
|
|
31
32
|
static getDependencies() { return []; }
|
|
32
33
|
|
|
33
34
|
process(context) {
|
|
34
|
-
|
|
35
|
-
const {
|
|
35
|
+
// Use the centralized engine injected via context
|
|
36
|
+
const { SmartMoneyScorer } = context.math;
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
const historyDoc = history.getDailyHistory(user);
|
|
39
|
-
|
|
40
|
-
// 2. Get strict summary DTO
|
|
41
|
-
const summary = history.getSummary(historyDoc);
|
|
38
|
+
if (!SmartMoneyScorer) return;
|
|
42
39
|
|
|
43
|
-
|
|
40
|
+
const result = SmartMoneyScorer.scoreHybrid(context);
|
|
41
|
+
const score = result.totalScore;
|
|
44
42
|
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const skillScore = expectancy * Math.log10(Math.max(1, summary.totalTrades));
|
|
52
|
-
|
|
53
|
-
if (isFinite(skillScore)) this.userScores.set(user.id, skillScore);
|
|
43
|
+
// GEM requires a minimum threshold of activity usually,
|
|
44
|
+
// but SmartMoneyScorer handles low-data cases by returning neutral/0.
|
|
45
|
+
if (score > 0) {
|
|
46
|
+
this.userScores.set(context.user.id, score);
|
|
47
|
+
}
|
|
54
48
|
}
|
|
55
49
|
|
|
56
50
|
getResult() {
|
|
@@ -49,8 +49,8 @@ class SqueezePotential {
|
|
|
49
49
|
|
|
50
50
|
for (const ticker of tickers) {
|
|
51
51
|
const shortSlAvg = signals.getMetric(computed, 'speculator-stop-loss-distance-by-ticker-short-long-breakdown', ticker, 'avg_short_sl_distance_pct');
|
|
52
|
-
const userCount
|
|
53
|
-
const weight
|
|
52
|
+
const userCount = signals.getMetric(computed, 'short-position-per-stock', ticker, 'short_count');
|
|
53
|
+
const weight = signals.getMetric(computed, 'short-position-per-stock', ticker, 'total_short_exposure_weight');
|
|
54
54
|
|
|
55
55
|
if (userCount > 0) {
|
|
56
56
|
result[ticker] = {
|