aiden-shared-calculations-unified 1.0.100 → 1.0.102
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.
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Core Calculation (Pass 2)
|
|
3
3
|
* Generates the "Ghost Book" - KDE of Holder Cost Basis.
|
|
4
|
-
*
|
|
4
|
+
* REFACTORED: Uses Trade History to reconstruct Open Interest (ghost positions).
|
|
5
|
+
* Solves sparse speculator data by using historical entry points.
|
|
5
6
|
*/
|
|
6
7
|
class AssetCostBasisProfile {
|
|
7
8
|
constructor() {
|
|
8
|
-
// Map<Ticker, Map<
|
|
9
|
+
// Map<Ticker, { bins: Map<Price, Weight>, currentPrice: Number }>
|
|
9
10
|
this.assetBins = new Map();
|
|
10
11
|
this.tickerMap = null;
|
|
11
|
-
//
|
|
12
|
+
// Store function pointer to avoid serialization issues
|
|
12
13
|
this.computeKDE_func = null;
|
|
14
|
+
this.updateforce = null;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
static getMetadata() {
|
|
16
18
|
return {
|
|
17
19
|
type: 'standard',
|
|
18
|
-
|
|
20
|
+
// Changed from 'portfolio' to 'history' and 'price' (needed for current valuation)
|
|
21
|
+
rootDataDependencies: ['history', 'price'],
|
|
19
22
|
dependencies: [],
|
|
20
23
|
isHistorical: false,
|
|
21
24
|
userType: 'all',
|
|
@@ -41,36 +44,78 @@ class AssetCostBasisProfile {
|
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
process(context) {
|
|
44
|
-
const { user, mappings, math } = context;
|
|
47
|
+
const { user, mappings, math, date, prices } = context;
|
|
45
48
|
|
|
46
|
-
// FIX 2 (CRITICAL): Store the *function pointer* itself from math.distribution.computeKDE.
|
|
47
|
-
// This is necessary because storing the class object (math.distribution) breaks the static method reference when serialized/deserialized by worker threads.
|
|
48
49
|
if (!this.computeKDE_func) {
|
|
49
50
|
this.computeKDE_func = math.distribution.computeKDE;
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
if (!this.tickerMap) this.tickerMap = mappings.instrumentToTicker;
|
|
53
54
|
|
|
54
|
-
const {
|
|
55
|
+
const { priceExtractor, history } = math;
|
|
55
56
|
|
|
56
|
-
|
|
57
|
+
// 1. Get History
|
|
58
|
+
const historyDoc = history.getDailyHistory(user);
|
|
59
|
+
if (!historyDoc || !Array.isArray(historyDoc.PublicHistoryPositions)) return;
|
|
60
|
+
|
|
61
|
+
const trades = historyDoc.PublicHistoryPositions;
|
|
62
|
+
const targetDate = new Date(date.today); // The date we are calculating FOR
|
|
63
|
+
|
|
64
|
+
for (const trade of trades) {
|
|
65
|
+
// 2. Validate Trade Data
|
|
66
|
+
const instId = trade.InstrumentID;
|
|
67
|
+
if (!instId) continue;
|
|
57
68
|
|
|
58
|
-
for (const pos of positions) {
|
|
59
|
-
const instId = extract.getInstrumentId(pos);
|
|
60
69
|
const ticker = this.tickerMap[instId];
|
|
61
|
-
|
|
62
70
|
if (!ticker) continue;
|
|
63
71
|
|
|
64
|
-
|
|
65
|
-
|
|
72
|
+
// 3. Time Travel Logic: Was this trade OPEN on the target date?
|
|
73
|
+
// Open Date must be BEFORE target
|
|
74
|
+
const openDate = new Date(trade.OpenDateTime);
|
|
75
|
+
if (isNaN(openDate) || openDate > targetDate) continue;
|
|
76
|
+
|
|
77
|
+
// Close Date must be AFTER target (or null/zero if still open in some schemas)
|
|
78
|
+
// Note: If CloseDateTime is missing, we assume it's still open or data is partial.
|
|
79
|
+
// We check if it's explicitly closed BEFORE target.
|
|
80
|
+
let isClosedBeforeTarget = false;
|
|
81
|
+
if (trade.CloseDateTime) {
|
|
82
|
+
const closeDate = new Date(trade.CloseDateTime);
|
|
83
|
+
if (!isNaN(closeDate) && closeDate < targetDate) {
|
|
84
|
+
isClosedBeforeTarget = true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (isClosedBeforeTarget) continue; // Trade was already closed, not part of "Cost Basis" for this day.
|
|
66
89
|
|
|
67
|
-
|
|
68
|
-
const
|
|
90
|
+
// 4. Get Cost Basis (Entry Price)
|
|
91
|
+
const entryPrice = trade.OpenRate;
|
|
92
|
+
if (!entryPrice || entryPrice <= 0) continue;
|
|
93
|
+
|
|
94
|
+
// 5. Determine Weight
|
|
95
|
+
// History often lacks 'Invested Amount', so we use Leverage as a proxy for conviction,
|
|
96
|
+
// or 1.0 if Leverage is missing.
|
|
97
|
+
const weight = (trade.Leverage && trade.Leverage > 0) ? trade.Leverage : 1.0;
|
|
98
|
+
|
|
99
|
+
// 6. Get Current Price (Contextual)
|
|
100
|
+
// We need the asset price on 'targetDate' to calculate bandwidth and store reference.
|
|
101
|
+
// This comes from the 'price' root dependency.
|
|
102
|
+
let currentPrice = 0;
|
|
103
|
+
const priceHistory = priceExtractor.getHistory(prices, instId);
|
|
69
104
|
|
|
70
|
-
|
|
105
|
+
// Find price for today
|
|
106
|
+
const priceObj = priceHistory.find(p => p.date === date.today);
|
|
107
|
+
if (priceObj) {
|
|
108
|
+
currentPrice = priceObj.price;
|
|
109
|
+
} else if (priceHistory.length > 0) {
|
|
110
|
+
// Fallback to latest available price if today is missing
|
|
111
|
+
currentPrice = priceHistory[priceHistory.length - 1].price;
|
|
112
|
+
}
|
|
71
113
|
|
|
72
|
-
|
|
114
|
+
if (!currentPrice || currentPrice <= 0) continue;
|
|
73
115
|
|
|
116
|
+
// 7. Aggregate
|
|
117
|
+
// We bucket by Entry Price (rounded) to build the distribution
|
|
118
|
+
// Round to 4 sig figs to group nearby entries
|
|
74
119
|
const priceKey = Number(entryPrice.toPrecision(6));
|
|
75
120
|
|
|
76
121
|
if (!this.assetBins.has(ticker)) {
|
|
@@ -78,6 +123,7 @@ class AssetCostBasisProfile {
|
|
|
78
123
|
}
|
|
79
124
|
const asset = this.assetBins.get(ticker);
|
|
80
125
|
|
|
126
|
+
// Keep updating current price (in case of multiple shards, though user is atomic)
|
|
81
127
|
asset.currentPrice = currentPrice;
|
|
82
128
|
|
|
83
129
|
const currentWeight = asset.bins.get(priceKey) || 0;
|
|
@@ -87,15 +133,14 @@ class AssetCostBasisProfile {
|
|
|
87
133
|
|
|
88
134
|
async getResult() {
|
|
89
135
|
const result = {};
|
|
90
|
-
|
|
91
|
-
// Use the function pointer stored in state. No longer need the corrupted 'DistributionAnalytics' variable.
|
|
92
136
|
const computeKDE = this.computeKDE_func;
|
|
93
137
|
|
|
94
|
-
if (!computeKDE) return {};
|
|
138
|
+
if (!computeKDE) return {};
|
|
95
139
|
|
|
96
140
|
for (const [ticker, data] of this.assetBins.entries()) {
|
|
97
141
|
const points = [];
|
|
98
142
|
let totalWeight = 0;
|
|
143
|
+
|
|
99
144
|
for (const [price, weight] of data.bins.entries()) {
|
|
100
145
|
points.push({ value: price, weight: weight });
|
|
101
146
|
totalWeight += weight;
|
|
@@ -103,15 +148,15 @@ class AssetCostBasisProfile {
|
|
|
103
148
|
|
|
104
149
|
if (points.length < 2) continue;
|
|
105
150
|
|
|
151
|
+
// Dynamic Bandwidth: 2% of price
|
|
106
152
|
const bandwidth = data.currentPrice * 0.02;
|
|
107
153
|
|
|
108
|
-
// FIX 3: Call the stored function directly
|
|
109
154
|
const profile = computeKDE(points, bandwidth, 60);
|
|
110
155
|
|
|
111
156
|
result[ticker] = {
|
|
112
157
|
profile: profile,
|
|
113
158
|
current_price: data.currentPrice,
|
|
114
|
-
total_inventory_weight: totalWeight
|
|
159
|
+
total_inventory_weight: totalWeight // Now represents Total Leverage/Count, not USD
|
|
115
160
|
};
|
|
116
161
|
}
|
|
117
162
|
return result;
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Ghost Book: Cost-Basis Density Estimation
|
|
3
|
-
* Identifies "Invisible Walls" of Support/Resistance based on
|
|
3
|
+
* Identifies "Invisible Walls" of Support/Resistance based on reconstructed historical entry points.
|
|
4
|
+
* Adapts to new History-based AssetCostBasisProfile.
|
|
4
5
|
*/
|
|
5
6
|
class CostBasisDensity {
|
|
6
7
|
constructor() { this.walls = {}; }
|
|
7
8
|
|
|
8
9
|
static getMetadata() {
|
|
9
10
|
return {
|
|
10
|
-
type: 'meta',
|
|
11
|
+
type: 'meta',
|
|
11
12
|
dependencies: ['asset-cost-basis-profile'],
|
|
12
|
-
userType: 'n/a',
|
|
13
|
-
isHistorical: false,
|
|
13
|
+
userType: 'n/a',
|
|
14
|
+
isHistorical: false,
|
|
14
15
|
category: 'ghost_book'
|
|
15
16
|
};
|
|
16
17
|
}
|
|
@@ -37,6 +38,7 @@ class CostBasisDensity {
|
|
|
37
38
|
const { computed, math } = context;
|
|
38
39
|
const { signals: SignalPrimitives } = math;
|
|
39
40
|
|
|
41
|
+
// This helper handles sharded re-assembly if needed
|
|
40
42
|
const tickers = SignalPrimitives.getUnionKeys(computed, ['asset-cost-basis-profile']);
|
|
41
43
|
|
|
42
44
|
for (const ticker of tickers) {
|
|
@@ -53,11 +55,13 @@ class CostBasisDensity {
|
|
|
53
55
|
const support = [];
|
|
54
56
|
let maxDensity = 0;
|
|
55
57
|
|
|
58
|
+
// Simple Peak Detection
|
|
56
59
|
for (let i = 1; i < profile.length - 1; i++) {
|
|
57
60
|
const prev = profile[i-1].density;
|
|
58
61
|
const curr = profile[i].density;
|
|
59
62
|
const next = profile[i+1].density;
|
|
60
63
|
|
|
64
|
+
// Is Peak?
|
|
61
65
|
if (curr > prev && curr > next) {
|
|
62
66
|
const priceVal = Number(profile[i].price.toFixed(2));
|
|
63
67
|
|
|
@@ -71,8 +75,13 @@ class CostBasisDensity {
|
|
|
71
75
|
}
|
|
72
76
|
}
|
|
73
77
|
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
// Sort nearest to current price?
|
|
79
|
+
// Usually standard practice is sorting by price levels ascending/descending
|
|
80
|
+
// Resistance: Low to High (Nearest first if above) -> Actually sorting Asc is fine
|
|
81
|
+
// Support: High to Low (Nearest first if below)
|
|
82
|
+
|
|
83
|
+
support.sort((a, b) => b - a); // Descending (Nearest support first)
|
|
84
|
+
resistance.sort((a, b) => a - b); // Ascending (Nearest resistance first)
|
|
76
85
|
|
|
77
86
|
this.walls[ticker] = {
|
|
78
87
|
resistance_zones: resistance.slice(0, 3),
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Ghost Book: Liquidity Vacuum
|
|
3
|
+
* Detects price zones with low historical entry density (Air Pockets).
|
|
4
|
+
* Updated to consume History-derived profiles.
|
|
5
|
+
*/
|
|
1
6
|
class LiquidityVacuum {
|
|
2
7
|
constructor() { this.vacuumResults = {}; }
|
|
3
8
|
|
|
@@ -5,8 +10,8 @@ class LiquidityVacuum {
|
|
|
5
10
|
return {
|
|
6
11
|
type: 'meta',
|
|
7
12
|
dependencies: ['asset-cost-basis-profile'],
|
|
8
|
-
userType: 'n/a',
|
|
9
|
-
isHistorical: false,
|
|
13
|
+
userType: 'n/a',
|
|
14
|
+
isHistorical: false,
|
|
10
15
|
category: 'ghost_book'
|
|
11
16
|
};
|
|
12
17
|
}
|
|
@@ -14,7 +19,19 @@ class LiquidityVacuum {
|
|
|
14
19
|
static getDependencies() { return ['asset-cost-basis-profile']; }
|
|
15
20
|
|
|
16
21
|
static getSchema() {
|
|
17
|
-
return {
|
|
22
|
+
return {
|
|
23
|
+
"type": "object",
|
|
24
|
+
"patternProperties": {
|
|
25
|
+
"^.*$": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"properties": {
|
|
28
|
+
"crash_probability": {"type":"number"},
|
|
29
|
+
"liquidity_ratio": {"type":"number"},
|
|
30
|
+
"status": {"type":"string"}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
18
35
|
}
|
|
19
36
|
|
|
20
37
|
process(context) {
|
|
@@ -28,19 +45,61 @@ class LiquidityVacuum {
|
|
|
28
45
|
if (!data || !data.profile || !Array.isArray(data.profile)) continue;
|
|
29
46
|
|
|
30
47
|
const current = data.current_price;
|
|
31
|
-
|
|
48
|
+
// This is now Sum(Leverage) or Sum(Count), not USD.
|
|
49
|
+
const totalInv = data.total_inventory_weight;
|
|
32
50
|
|
|
51
|
+
// Calculate density strictly below current price (5% drop zone)
|
|
33
52
|
const riskVol = distribution.integrateProfile(data.profile, current * 0.95, current);
|
|
53
|
+
|
|
54
|
+
// Ratio: How much "Support" (Cost Basis Density) exists in the drop zone relative to total holding?
|
|
34
55
|
const ratio = (totalInv > 0) ? (riskVol / totalInv) * 10 : 0;
|
|
35
56
|
|
|
36
57
|
let status = "STABLE";
|
|
37
|
-
|
|
38
|
-
|
|
58
|
+
// If ratio is LOW, it means there is very little cost basis support below price -> VACUUM
|
|
59
|
+
// Wait, previous logic was: ratio > 0.5 = FRAGILE?
|
|
60
|
+
// If riskVol is HIGH, it means lots of support.
|
|
61
|
+
// Let's invert the logic or verify the metric meaning.
|
|
62
|
+
// Original: ratio = (riskVol/totalInv).
|
|
63
|
+
// If riskVol is high (lots of density below), ratio is high.
|
|
64
|
+
// High Density below = Strong Support.
|
|
65
|
+
// Low Density below = Vacuum.
|
|
66
|
+
|
|
67
|
+
// Correction: Previous logic likely inverted naming or I misinterpreted "Vacuum".
|
|
68
|
+
// A "Liquidity Vacuum" implies LOW density.
|
|
69
|
+
// Let's implement Vacuum Logic: Low Density = High Crash Probability.
|
|
70
|
+
|
|
71
|
+
// Inverted Ratio for "Vacuum Strength"
|
|
72
|
+
// If riskVol is near 0, Vacuum is High.
|
|
73
|
+
|
|
74
|
+
// Let's stick to the previous implementation's thresholding to avoid breaking existing dashboards,
|
|
75
|
+
// assuming "Liquidity Ratio" meant "Vacuum Ratio" previously.
|
|
76
|
+
// IF previous code: if (ratio > 0.5) status = "FRAGILE";
|
|
77
|
+
// Then High Ratio = Bad.
|
|
78
|
+
// This implies High Density just below price = Bad?
|
|
79
|
+
// That corresponds to "Bag Holders waiting to break even" if price drops?
|
|
80
|
+
// Or "Weak Hands"?
|
|
81
|
+
|
|
82
|
+
// Let's stick to the standard Volume Profile theory:
|
|
83
|
+
// Low Volume Node (LVN) = Fast movement (Vacuum).
|
|
84
|
+
// High Volume Node (HVN) = Support/Resistance (Slow movement).
|
|
85
|
+
|
|
86
|
+
// I will update the logic to be explicitly clear for the History-based model:
|
|
87
|
+
// We want to detect if there is NO cost basis support below.
|
|
88
|
+
|
|
89
|
+
const supportDensity = riskVol / totalInv; // 0.0 to 1.0
|
|
90
|
+
|
|
91
|
+
let vacuumScore = 0;
|
|
92
|
+
if (supportDensity < 0.05) vacuumScore = 1.0; // Critical Vacuum (No support)
|
|
93
|
+
else if (supportDensity < 0.15) vacuumScore = 0.5; // Moderate Vacuum
|
|
94
|
+
|
|
95
|
+
let statusLabel = "STABLE";
|
|
96
|
+
if (vacuumScore > 0.8) statusLabel = "CRITICAL_VACUUM";
|
|
97
|
+
else if (vacuumScore > 0.4) statusLabel = "FRAGILE";
|
|
39
98
|
|
|
40
99
|
this.vacuumResults[ticker] = {
|
|
41
|
-
crash_probability: Number(
|
|
42
|
-
liquidity_ratio: Number(
|
|
43
|
-
status:
|
|
100
|
+
crash_probability: Number(vacuumScore.toFixed(2)),
|
|
101
|
+
liquidity_ratio: Number(supportDensity.toFixed(4)), // Reporting actual density now
|
|
102
|
+
status: statusLabel
|
|
44
103
|
};
|
|
45
104
|
}
|
|
46
105
|
}
|
package/package.json
CHANGED
|
File without changes
|