aiden-shared-calculations-unified 1.0.106 → 1.0.107
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/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 +130 -83
- package/calculations/gauss/daily-dna-filter.js +13 -24
- package/calculations/gem/cohort-skill-definition.js +13 -19
- package/package.json +1 -1
|
@@ -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,175 @@ 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
|
-
// "Held" means active at End of Day
|
|
80
|
-
const isHeldAtEOD = isOpenBeforeEOD && (closeTime === null || closeTime > dayEnd);
|
|
81
|
-
|
|
82
|
-
// "Opened" means OpenTime is inside today
|
|
83
|
-
const isOpenedToday = openTime >= dayStart && openTime <= dayEnd;
|
|
84
|
-
|
|
85
|
-
// "Closed" means CloseTime is inside today
|
|
86
|
-
const isClosedToday = closeTime !== null && closeTime >= dayStart && closeTime <= dayEnd;
|
|
87
|
-
|
|
88
|
-
if (!wasActiveToday) continue;
|
|
89
|
-
|
|
90
|
-
if (!userState[ticker]) {
|
|
91
|
-
userState[ticker] = {
|
|
92
|
-
isHolder: 0,
|
|
93
|
-
didBuy: 0,
|
|
94
|
-
didSell: 0,
|
|
95
|
-
sumEntry: 0,
|
|
96
|
-
sumLev: 0,
|
|
97
|
-
holdCount: 0,
|
|
98
|
-
sumBuyLev: 0,
|
|
99
|
-
closeReasons: { "0": 0, "1": 0, "5": 0 }
|
|
100
|
-
};
|
|
81
|
+
// Add OPEN event
|
|
82
|
+
events.push({ time: openTime, type: 'OPEN', trade, ticker });
|
|
83
|
+
|
|
84
|
+
// Add CLOSE event (if closed)
|
|
85
|
+
if (closeTime) {
|
|
86
|
+
events.push({ time: closeTime, type: 'CLOSE', trade, ticker });
|
|
101
87
|
}
|
|
102
|
-
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Sort events by time
|
|
91
|
+
events.sort((a, b) => a.time - b.time);
|
|
92
|
+
|
|
93
|
+
if (events.length === 0) return;
|
|
103
94
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
95
|
+
// 3. Scan-Line Execution (Replay History)
|
|
96
|
+
const userTimeline = {}; // { "2024-01-01": { "AAPL": {...} } }
|
|
97
|
+
|
|
98
|
+
// Tracking State
|
|
99
|
+
const activePositions = new Map(); // Map<PositionID, Trade>
|
|
100
|
+
|
|
101
|
+
// Determine Start and End dates for the loop
|
|
102
|
+
const firstEventTime = events[0].time;
|
|
103
|
+
const lastEventTime = events[events.length - 1].time;
|
|
104
|
+
|
|
105
|
+
const startDate = new Date(firstEventTime); startDate.setUTCHours(0,0,0,0);
|
|
106
|
+
const endDate = new Date(lastEventTime); endDate.setUTCHours(0,0,0,0);
|
|
107
|
+
|
|
108
|
+
let currentEventIdx = 0;
|
|
109
|
+
const oneDayMs = 86400000;
|
|
110
|
+
|
|
111
|
+
// Iterate day by day from first trade to last trade
|
|
112
|
+
for (let d = startDate.getTime(); d <= endDate.getTime(); d += oneDayMs) {
|
|
113
|
+
const dateStr = new Date(d).toISOString().slice(0, 10);
|
|
114
|
+
const dayEnd = d + oneDayMs - 1;
|
|
115
|
+
|
|
116
|
+
const dayStats = {}; // Ticker -> Stats
|
|
117
|
+
|
|
118
|
+
// Process all events that happened TODAY (before EOD)
|
|
119
|
+
while (currentEventIdx < events.length && events[currentEventIdx].time <= dayEnd) {
|
|
120
|
+
const event = events[currentEventIdx];
|
|
121
|
+
const { ticker, trade, type } = event;
|
|
122
|
+
|
|
123
|
+
if (!dayStats[ticker]) this.initTickerStats(dayStats, ticker);
|
|
124
|
+
|
|
125
|
+
if (type === 'OPEN') {
|
|
126
|
+
activePositions.set(trade.PositionID, trade);
|
|
127
|
+
dayStats[ticker].didBuy++;
|
|
128
|
+
dayStats[ticker].sumBuyLev += (trade.Leverage || 1);
|
|
129
|
+
} else if (type === 'CLOSE') {
|
|
130
|
+
activePositions.delete(trade.PositionID);
|
|
131
|
+
dayStats[ticker].didSell++;
|
|
132
|
+
|
|
133
|
+
const reason = String(trade.CloseReason || 0);
|
|
134
|
+
if (dayStats[ticker].closeReasons[reason] === undefined) dayStats[ticker].closeReasons[reason] = 0;
|
|
135
|
+
dayStats[ticker].closeReasons[reason]++;
|
|
136
|
+
}
|
|
137
|
+
currentEventIdx++;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Snapshot the "Held" State at EOD
|
|
141
|
+
for (const [posId, trade] of activePositions) {
|
|
142
|
+
const ticker = mappings.instrumentToTicker[trade.InstrumentID];
|
|
143
|
+
if (!ticker) continue;
|
|
144
|
+
|
|
145
|
+
if (!dayStats[ticker]) this.initTickerStats(dayStats, ticker);
|
|
146
|
+
|
|
147
|
+
const stats = dayStats[ticker];
|
|
148
|
+
stats.isHolder = 1; // User is a holder today
|
|
107
149
|
stats.holdCount++;
|
|
108
150
|
stats.sumEntry += (trade.OpenRate || 0);
|
|
109
151
|
stats.sumLev += (trade.Leverage || 1);
|
|
110
152
|
}
|
|
111
153
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
stats
|
|
115
|
-
|
|
154
|
+
// Finalize Averages for this Day
|
|
155
|
+
for (const ticker in dayStats) {
|
|
156
|
+
const stats = dayStats[ticker];
|
|
157
|
+
|
|
158
|
+
if (stats.holdCount > 0) {
|
|
159
|
+
stats.avgEntry = stats.sumEntry / stats.holdCount;
|
|
160
|
+
stats.avgLeverage = stats.sumLev / stats.holdCount;
|
|
161
|
+
}
|
|
162
|
+
if (stats.didBuy > 0) {
|
|
163
|
+
stats.buyLeverage = stats.sumBuyLev / stats.didBuy;
|
|
164
|
+
}
|
|
116
165
|
|
|
117
|
-
|
|
118
|
-
stats.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
stats.
|
|
166
|
+
// Cleanup temp sums
|
|
167
|
+
delete stats.sumEntry;
|
|
168
|
+
delete stats.sumLev;
|
|
169
|
+
delete stats.sumBuyLev;
|
|
170
|
+
delete stats.holdCount;
|
|
122
171
|
}
|
|
123
|
-
}
|
|
124
172
|
|
|
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;
|
|
173
|
+
// Store in timeline if any activity exists for this day
|
|
174
|
+
if (Object.keys(dayStats).length > 0) {
|
|
175
|
+
userTimeline[dateStr] = dayStats;
|
|
135
176
|
}
|
|
177
|
+
}
|
|
136
178
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
179
|
+
// 4. Output the full timeline for this user
|
|
180
|
+
// The ResultCommitter will handle splitting this into date buckets.
|
|
181
|
+
this.results[user.id] = userTimeline;
|
|
182
|
+
}
|
|
142
183
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
184
|
+
initTickerStats(dayStats, ticker) {
|
|
185
|
+
dayStats[ticker] = {
|
|
186
|
+
isHolder: 0,
|
|
187
|
+
didBuy: 0,
|
|
188
|
+
didSell: 0,
|
|
189
|
+
sumEntry: 0,
|
|
190
|
+
sumLev: 0,
|
|
191
|
+
holdCount: 0,
|
|
192
|
+
sumBuyLev: 0,
|
|
193
|
+
closeReasons: { "0": 0, "1": 0, "5": 0 }
|
|
194
|
+
};
|
|
148
195
|
}
|
|
149
196
|
|
|
150
197
|
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() {
|