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
- * ARCHITECTURE COMPLIANT: Uses Context for Math & Prices.
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<PriceKey, Weight>>
9
+ // Map<Ticker, { bins: Map<Price, Weight>, currentPrice: Number }>
9
10
  this.assetBins = new Map();
10
11
  this.tickerMap = null;
11
- // FIX 1: Change state name to clearly indicate storage of the function pointer
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
- rootDataDependencies: ['portfolio'],
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 { extract } = math;
55
+ const { priceExtractor, history } = math;
55
56
 
56
- const positions = extract.getPositions(user.portfolio.today, user.type);
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
- const currentPrice = extract.getCurrentRate(pos);
65
- if (currentPrice <= 0) continue;
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
- const netProfitPct = extract.getNetProfit(pos);
68
- const weight = extract.getPositionWeight(pos, user.type);
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
- if (netProfitPct <= -99.9) continue;
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
- const entryPrice = currentPrice / (1 + (netProfitPct / 100.0));
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 {}; // Safety guard
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 holder break-even psychology.
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', // Runs ONCE per day
11
+ type: 'meta',
11
12
  dependencies: ['asset-cost-basis-profile'],
12
- userType: 'n/a', // FIXED: Added missing field
13
- isHistorical: false, // FIXED: Explicitly defined
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
- support.sort((a, b) => b - a);
75
- resistance.sort((a, b) => a - b);
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', // FIXED: Added missing field
9
- isHistorical: false, // FIXED: Explicitly defined
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 { "type": "object", "patternProperties": { "^.*$": { "type": "object", "properties": { "crash_probability": {"type":"number"}, "liquidity_ratio": {"type":"number"} } } } };
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
- const totalInv = data.total_inventory_weight;
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
- if (ratio > 0.5) status = "FRAGILE";
38
- if (ratio > 1.0) status = "CRITICAL_VACUUM";
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(Math.min(ratio * 0.5, 0.99).toFixed(2)),
42
- liquidity_ratio: Number(ratio.toFixed(4)),
43
- status: 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiden-shared-calculations-unified",
3
- "version": "1.0.100",
3
+ "version": "1.0.102",
4
4
  "description": "Shared calculation modules for the BullTrackers Computation System.",
5
5
  "main": "index.js",
6
6
  "files": [
File without changes