aiden-shared-calculations-unified 1.0.87 → 1.0.88
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/capitulation/asset-volatility-estimator.js +96 -0
- package/calculations/capitulation/retail-capitulation-risk-forecast.js +173 -0
- package/calculations/core/asset-cost-basis-profile.js +127 -0
- package/calculations/core/platform-daily-ownership-delta.js +1 -1
- package/calculations/core/test..js +0 -0
- package/calculations/ghost-book/cost-basis-density.js +79 -0
- package/calculations/ghost-book/liquidity-vacuum.js +52 -0
- package/calculations/ghost-book/retail-gamma-exposure.js +86 -0
- package/calculations/predicative-alpha/cognitive-dissonance.js +113 -0
- package/calculations/predicative-alpha/diamond-hand-fracture.js +90 -0
- package/calculations/predicative-alpha/mimetic-latency.js +124 -0
- package/package.json +1 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview CORE Product Line (Pass 1 - Meta)
|
|
3
|
+
* Calculates annualized volatility using the new priceExtractor.
|
|
4
|
+
*/
|
|
5
|
+
class AssetVolatilityEstimator {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.result = {};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
static getMetadata() {
|
|
11
|
+
return {
|
|
12
|
+
type: 'meta',
|
|
13
|
+
rootDataDependencies: ['price'],
|
|
14
|
+
isHistorical: false,
|
|
15
|
+
userType: 'n/a',
|
|
16
|
+
category: 'market_stats'
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static getDependencies() { return []; }
|
|
21
|
+
|
|
22
|
+
static getSchema() {
|
|
23
|
+
const tickerSchema = {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"properties": {
|
|
26
|
+
"volatility_30d": { "type": "number" },
|
|
27
|
+
"last_price": { "type": "number" },
|
|
28
|
+
"data_points": { "type": "number" }
|
|
29
|
+
},
|
|
30
|
+
"required": ["volatility_30d"]
|
|
31
|
+
};
|
|
32
|
+
return { "type": "object", "patternProperties": { "^.*$": tickerSchema } };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
process(context) {
|
|
36
|
+
// FIXED: Destructure 'mappings' to resolve real tickers
|
|
37
|
+
const { math, prices, mappings } = context;
|
|
38
|
+
|
|
39
|
+
// FIXED: Destructure 'priceExtractor' directly (matches worker.js injection)
|
|
40
|
+
const { compute, priceExtractor } = math;
|
|
41
|
+
|
|
42
|
+
// 1. Get ALL histories properly sorted and parsed
|
|
43
|
+
const allHistories = priceExtractor.getAllHistories(prices);
|
|
44
|
+
|
|
45
|
+
for (const [key, candles] of allHistories.entries()) {
|
|
46
|
+
// RESOLUTION FIX:
|
|
47
|
+
// 'key' is likely an index string ("0") because mock data is an array.
|
|
48
|
+
// We must resolve this to the real Ticker Symbol for the result map.
|
|
49
|
+
let ticker = key;
|
|
50
|
+
|
|
51
|
+
if (prices.history && prices.history[key] && prices.history[key].instrumentId) {
|
|
52
|
+
const instId = prices.history[key].instrumentId;
|
|
53
|
+
if (mappings && mappings.instrumentToTicker && mappings.instrumentToTicker[instId]) {
|
|
54
|
+
ticker = mappings.instrumentToTicker[instId];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (candles.length < 10) continue;
|
|
59
|
+
|
|
60
|
+
// 2. Calculate Log Returns
|
|
61
|
+
const logReturns = [];
|
|
62
|
+
let lastPrice = 0;
|
|
63
|
+
|
|
64
|
+
for (let i = 1; i < candles.length; i++) {
|
|
65
|
+
const prev = candles[i-1].price;
|
|
66
|
+
const curr = candles[i].price;
|
|
67
|
+
|
|
68
|
+
if (prev > 0 && curr > 0) {
|
|
69
|
+
logReturns.push(Math.log(curr / prev));
|
|
70
|
+
lastPrice = curr;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 3. Filter 30-Day Lookback
|
|
75
|
+
const LOOKBACK = 30;
|
|
76
|
+
const relevantReturns = logReturns.slice(-LOOKBACK);
|
|
77
|
+
|
|
78
|
+
if (relevantReturns.length < 5) continue;
|
|
79
|
+
|
|
80
|
+
// 4. Calculate Stats
|
|
81
|
+
const stdDev = compute.standardDeviation(relevantReturns);
|
|
82
|
+
const annualizedVol = stdDev * Math.sqrt(365);
|
|
83
|
+
|
|
84
|
+
this.result[ticker] = {
|
|
85
|
+
volatility_30d: annualizedVol,
|
|
86
|
+
last_price: lastPrice,
|
|
87
|
+
data_points: relevantReturns.length
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async getResult() { return this.result; }
|
|
93
|
+
reset() { this.result = {}; }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = AssetVolatilityEstimator;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview CORE Product Line (Pass 2 - Standard)
|
|
3
|
+
* Calculates capitulation risk using DYNAMIC volatility from asset-volatility-estimator.
|
|
4
|
+
* Relies on 'schema.md' definitions for portfolio extraction via MathPrimitives.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class RetailCapitulationRiskForecast {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.assetRiskProfiles = new Map();
|
|
10
|
+
this.tickerMap = null;
|
|
11
|
+
|
|
12
|
+
// FIX: Use a container object (like the Maps in other calculations)
|
|
13
|
+
// This allows us to MUTATE 'deps.mathLib' instead of RE-ASSIGNING 'this.mathLib'
|
|
14
|
+
this.deps = { mathLib: null };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static getMetadata() {
|
|
18
|
+
return {
|
|
19
|
+
type: 'standard',
|
|
20
|
+
rootDataDependencies: ['portfolio', 'history'],
|
|
21
|
+
isHistorical: false,
|
|
22
|
+
userType: 'all',
|
|
23
|
+
category: 'risk_models'
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static getDependencies() {
|
|
28
|
+
return ['asset-volatility-estimator'];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static getSchema() {
|
|
32
|
+
const tickerSchema = {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"properties": {
|
|
35
|
+
"capitulation_probability": { "type": "number" },
|
|
36
|
+
"at_risk_user_count": { "type": "number" },
|
|
37
|
+
"average_pain_threshold_pct": { "type": "number" },
|
|
38
|
+
"used_volatility": { "type": "number" }
|
|
39
|
+
},
|
|
40
|
+
"required": ["capitulation_probability", "at_risk_user_count"]
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return { "type": "object", "patternProperties": { "^.*$": tickerSchema } };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_initAsset(ticker, currentPrice, volatility) {
|
|
47
|
+
if (!this.assetRiskProfiles.has(ticker)) {
|
|
48
|
+
this.assetRiskProfiles.set(ticker, {
|
|
49
|
+
currentPrice: currentPrice,
|
|
50
|
+
volatility: volatility,
|
|
51
|
+
profiles: []
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
process(context) {
|
|
57
|
+
const { user, mappings, math, computed } = context;
|
|
58
|
+
const { extract, history, compute, signals } = math;
|
|
59
|
+
|
|
60
|
+
// 1. Capture Libraries via MUTATION
|
|
61
|
+
// We are retrieving the 'deps' object (GET) and mutating its property.
|
|
62
|
+
// This bypasses the Proxy SET trap entirely.
|
|
63
|
+
if (!this.deps.mathLib && compute) {
|
|
64
|
+
this.deps.mathLib = compute;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!this.tickerMap) {
|
|
68
|
+
this.tickerMap = mappings.instrumentToTicker;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 2. Determine User's "Pain Threshold"
|
|
72
|
+
const historyDoc = history.getDailyHistory(user);
|
|
73
|
+
const summary = history.getSummary(historyDoc);
|
|
74
|
+
let personalPainThreshold = (summary && summary.avgLossPct < 0)
|
|
75
|
+
? summary.avgLossPct
|
|
76
|
+
: -25.0;
|
|
77
|
+
|
|
78
|
+
// 3. Analyze Positions
|
|
79
|
+
const positions = extract.getPositions(user.portfolio.today, user.type);
|
|
80
|
+
|
|
81
|
+
for (const pos of positions) {
|
|
82
|
+
const instId = extract.getInstrumentId(pos);
|
|
83
|
+
|
|
84
|
+
if (!this.tickerMap) continue;
|
|
85
|
+
|
|
86
|
+
const ticker = this.tickerMap[instId];
|
|
87
|
+
|
|
88
|
+
if (!ticker) continue;
|
|
89
|
+
|
|
90
|
+
// Fetch Dependency
|
|
91
|
+
const assetStats = signals.getPreviousState(computed, 'asset-volatility-estimator', ticker);
|
|
92
|
+
|
|
93
|
+
const dynamicVol = assetStats ? assetStats.volatility_30d : 0.60;
|
|
94
|
+
const currentPrice = assetStats ? assetStats.last_price : 0;
|
|
95
|
+
|
|
96
|
+
if (currentPrice <= 0) continue;
|
|
97
|
+
|
|
98
|
+
// Get P&L from Position Schema
|
|
99
|
+
const netProfit = extract.getNetProfit(pos);
|
|
100
|
+
|
|
101
|
+
// Calculate Entry Price using the Dependency Price
|
|
102
|
+
const entryPrice = extract.deriveEntryPrice(currentPrice, netProfit);
|
|
103
|
+
|
|
104
|
+
if (entryPrice > 0) {
|
|
105
|
+
this._initAsset(ticker, currentPrice, dynamicVol);
|
|
106
|
+
this.assetRiskProfiles.get(ticker).profiles.push({
|
|
107
|
+
entryPrice: entryPrice,
|
|
108
|
+
thresholdPct: personalPainThreshold
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async getResult() {
|
|
115
|
+
const result = {};
|
|
116
|
+
const TIME_HORIZON_DAYS = 3;
|
|
117
|
+
const SIMULATION_COUNT = 1000;
|
|
118
|
+
|
|
119
|
+
// Access the library from the container
|
|
120
|
+
const mathLib = this.deps.mathLib;
|
|
121
|
+
|
|
122
|
+
if (!mathLib || !mathLib.simulateGBM) {
|
|
123
|
+
console.log('[DEBUG RCRF] MathLib missing in deps container!');
|
|
124
|
+
return {};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const [ticker, data] of this.assetRiskProfiles.entries()) {
|
|
128
|
+
if (data.profiles.length < 1) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
// 1. Generate Price Paths
|
|
134
|
+
const pricePaths = mathLib.simulateGBM(
|
|
135
|
+
data.currentPrice,
|
|
136
|
+
data.volatility,
|
|
137
|
+
TIME_HORIZON_DAYS,
|
|
138
|
+
SIMULATION_COUNT
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
if (!pricePaths || pricePaths.length === 0) continue;
|
|
142
|
+
|
|
143
|
+
// 2. Run Population Breakdown
|
|
144
|
+
const capitulationProb = mathLib.simulatePopulationBreakdown(
|
|
145
|
+
pricePaths,
|
|
146
|
+
data.profiles
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const totalThreshold = data.profiles.reduce((acc, p) => acc + p.thresholdPct, 0);
|
|
150
|
+
const avgThreshold = totalThreshold / data.profiles.length;
|
|
151
|
+
|
|
152
|
+
result[ticker] = {
|
|
153
|
+
capitulation_probability: parseFloat(capitulationProb.toFixed(4)),
|
|
154
|
+
at_risk_user_count: data.profiles.length,
|
|
155
|
+
average_pain_threshold_pct: parseFloat(avgThreshold.toFixed(2)),
|
|
156
|
+
used_volatility: parseFloat(data.volatility.toFixed(4))
|
|
157
|
+
};
|
|
158
|
+
} catch (err) {
|
|
159
|
+
console.log(`[DEBUG RCRF] Error processing ${ticker}: ${err.message}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
reset() {
|
|
167
|
+
this.assetRiskProfiles.clear();
|
|
168
|
+
this.tickerMap = null;
|
|
169
|
+
this.deps.mathLib = null; // Reset the container
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = RetailCapitulationRiskForecast;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Core Calculation (Pass 2)
|
|
3
|
+
* Generates the "Ghost Book" - KDE of Holder Cost Basis.
|
|
4
|
+
* ARCHITECTURE COMPLIANT: Uses Context for Math & Prices.
|
|
5
|
+
*/
|
|
6
|
+
class AssetCostBasisProfile {
|
|
7
|
+
constructor() {
|
|
8
|
+
// Map<Ticker, Map<PriceKey, Weight>>
|
|
9
|
+
this.assetBins = new Map();
|
|
10
|
+
this.tickerMap = null;
|
|
11
|
+
// FIX 1: Change state name to clearly indicate storage of the function pointer
|
|
12
|
+
this.computeKDE_func = null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
static getMetadata() {
|
|
16
|
+
return {
|
|
17
|
+
type: 'standard',
|
|
18
|
+
rootDataDependencies: ['portfolio'],
|
|
19
|
+
dependencies: [],
|
|
20
|
+
isHistorical: false,
|
|
21
|
+
userType: 'all',
|
|
22
|
+
category: 'core_distribution'
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static getDependencies() { return []; }
|
|
27
|
+
|
|
28
|
+
static getSchema() {
|
|
29
|
+
const profileSchema = {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"properties": {
|
|
32
|
+
"profile": {
|
|
33
|
+
"type": "array",
|
|
34
|
+
"items": { "type": "object", "properties": { "price": {"type": "number"}, "density": {"type": "number"} } }
|
|
35
|
+
},
|
|
36
|
+
"current_price": { "type": "number" },
|
|
37
|
+
"total_inventory_weight": { "type": "number" }
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
return { "type": "object", "patternProperties": { "^.*$": profileSchema } };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
process(context) {
|
|
44
|
+
const { user, mappings, math } = context;
|
|
45
|
+
|
|
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
|
+
if (!this.computeKDE_func) {
|
|
49
|
+
this.computeKDE_func = math.distribution.computeKDE;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!this.tickerMap) this.tickerMap = mappings.instrumentToTicker;
|
|
53
|
+
|
|
54
|
+
const { extract } = math;
|
|
55
|
+
|
|
56
|
+
const positions = extract.getPositions(user.portfolio.today, user.type);
|
|
57
|
+
|
|
58
|
+
for (const pos of positions) {
|
|
59
|
+
const instId = extract.getInstrumentId(pos);
|
|
60
|
+
const ticker = this.tickerMap[instId];
|
|
61
|
+
|
|
62
|
+
if (!ticker) continue;
|
|
63
|
+
|
|
64
|
+
const currentPrice = extract.getCurrentRate(pos);
|
|
65
|
+
if (currentPrice <= 0) continue;
|
|
66
|
+
|
|
67
|
+
const netProfitPct = extract.getNetProfit(pos);
|
|
68
|
+
const weight = extract.getPositionWeight(pos, user.type);
|
|
69
|
+
|
|
70
|
+
if (netProfitPct <= -99.9) continue;
|
|
71
|
+
|
|
72
|
+
const entryPrice = currentPrice / (1 + (netProfitPct / 100.0));
|
|
73
|
+
|
|
74
|
+
const priceKey = Number(entryPrice.toPrecision(6));
|
|
75
|
+
|
|
76
|
+
if (!this.assetBins.has(ticker)) {
|
|
77
|
+
this.assetBins.set(ticker, { bins: new Map(), currentPrice });
|
|
78
|
+
}
|
|
79
|
+
const asset = this.assetBins.get(ticker);
|
|
80
|
+
|
|
81
|
+
asset.currentPrice = currentPrice;
|
|
82
|
+
|
|
83
|
+
const currentWeight = asset.bins.get(priceKey) || 0;
|
|
84
|
+
asset.bins.set(priceKey, currentWeight + weight);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async getResult() {
|
|
89
|
+
const result = {};
|
|
90
|
+
|
|
91
|
+
// Use the function pointer stored in state. No longer need the corrupted 'DistributionAnalytics' variable.
|
|
92
|
+
const computeKDE = this.computeKDE_func;
|
|
93
|
+
|
|
94
|
+
if (!computeKDE) return {}; // Safety guard
|
|
95
|
+
|
|
96
|
+
for (const [ticker, data] of this.assetBins.entries()) {
|
|
97
|
+
const points = [];
|
|
98
|
+
let totalWeight = 0;
|
|
99
|
+
for (const [price, weight] of data.bins.entries()) {
|
|
100
|
+
points.push({ value: price, weight: weight });
|
|
101
|
+
totalWeight += weight;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (points.length < 2) continue;
|
|
105
|
+
|
|
106
|
+
const bandwidth = data.currentPrice * 0.02;
|
|
107
|
+
|
|
108
|
+
// FIX 3: Call the stored function directly
|
|
109
|
+
const profile = computeKDE(points, bandwidth, 60);
|
|
110
|
+
|
|
111
|
+
result[ticker] = {
|
|
112
|
+
profile: profile,
|
|
113
|
+
current_price: data.currentPrice,
|
|
114
|
+
total_inventory_weight: totalWeight
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
reset() {
|
|
121
|
+
this.assetBins.clear();
|
|
122
|
+
this.tickerMap = null;
|
|
123
|
+
this.computeKDE_func = null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = AssetCostBasisProfile;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Calculation (Pass 1) for ownership delta (users added/lost per asset).
|
|
3
|
-
* REFACTORED: Tracks net change in number of owners.
|
|
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
4
|
*/
|
|
5
5
|
class PlatformDailyOwnershipDelta {
|
|
6
6
|
constructor() {
|
|
File without changes
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Ghost Book: Cost-Basis Density Estimation
|
|
3
|
+
* Identifies "Invisible Walls" of Support/Resistance based on holder break-even psychology.
|
|
4
|
+
*/
|
|
5
|
+
class CostBasisDensity {
|
|
6
|
+
constructor() { this.walls = {}; }
|
|
7
|
+
|
|
8
|
+
static getMetadata() {
|
|
9
|
+
return {
|
|
10
|
+
type: 'meta',
|
|
11
|
+
dependencies: ['asset-cost-basis-profile'],
|
|
12
|
+
category: 'ghost_book'
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static getDependencies() { return ['asset-cost-basis-profile']; }
|
|
17
|
+
|
|
18
|
+
static getSchema() {
|
|
19
|
+
return {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"patternProperties": {
|
|
22
|
+
"^.*$": {
|
|
23
|
+
"type": "object",
|
|
24
|
+
"properties": {
|
|
25
|
+
"resistance_zones": { "type": "array", "items": { "type": "number" } },
|
|
26
|
+
"support_zones": { "type": "array", "items": { "type": "number" } },
|
|
27
|
+
"nearest_wall_strength": { "type": "number" }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
process(context) {
|
|
35
|
+
const { computed, math } = context;
|
|
36
|
+
const { signals: SignalPrimitives } = math;
|
|
37
|
+
|
|
38
|
+
const tickers = SignalPrimitives.getUnionKeys(computed, ['asset-cost-basis-profile']);
|
|
39
|
+
|
|
40
|
+
for (const ticker of tickers) {
|
|
41
|
+
const data = computed['asset-cost-basis-profile'][ticker];
|
|
42
|
+
if (!data || !data.profile) continue;
|
|
43
|
+
|
|
44
|
+
const profile = data.profile; // Array of {price, density}
|
|
45
|
+
const currentPrice = data.current_price;
|
|
46
|
+
|
|
47
|
+
const resistance = [];
|
|
48
|
+
const support = [];
|
|
49
|
+
let maxDensity = 0;
|
|
50
|
+
|
|
51
|
+
// Find Local Maxima in the Density Curve
|
|
52
|
+
for (let i = 1; i < profile.length - 1; i++) {
|
|
53
|
+
const prev = profile[i-1].density;
|
|
54
|
+
const curr = profile[i].density;
|
|
55
|
+
const next = profile[i+1].density;
|
|
56
|
+
|
|
57
|
+
if (curr > prev && curr > next) {
|
|
58
|
+
// We have a peak (Wall)
|
|
59
|
+
if (profile[i].price > currentPrice) {
|
|
60
|
+
resistance.push(Number(profile[i].price.toFixed(2)));
|
|
61
|
+
} else {
|
|
62
|
+
support.push(Number(profile[i].price.toFixed(2)));
|
|
63
|
+
}
|
|
64
|
+
if (curr > maxDensity) maxDensity = curr;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.walls[ticker] = {
|
|
69
|
+
resistance_zones: resistance.slice(0, 3), // Top 3
|
|
70
|
+
support_zones: support.slice(0, 3),
|
|
71
|
+
nearest_wall_strength: Number(maxDensity.toFixed(4))
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getResult() { return this.walls; }
|
|
77
|
+
reset() { this.walls = {}; }
|
|
78
|
+
}
|
|
79
|
+
module.exports = CostBasisDensity;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
class LiquidityVacuum {
|
|
2
|
+
constructor() { this.vacuumResults = {}; }
|
|
3
|
+
|
|
4
|
+
static getMetadata() {
|
|
5
|
+
return {
|
|
6
|
+
type: 'meta',
|
|
7
|
+
dependencies: ['asset-cost-basis-profile'],
|
|
8
|
+
category: 'ghost_book'
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static getDependencies() { return ['asset-cost-basis-profile']; }
|
|
13
|
+
|
|
14
|
+
static getSchema() {
|
|
15
|
+
return { "type": "object", "patternProperties": { "^.*$": { "type": "object", "properties": { "crash_probability": {"type":"number"}, "liquidity_ratio": {"type":"number"} } } } };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
process(context) {
|
|
19
|
+
const { computed, math } = context;
|
|
20
|
+
const { distribution } = math; // Correctly accesses 'distribution' from context
|
|
21
|
+
|
|
22
|
+
const tickers = Object.keys(computed['asset-cost-basis-profile'] || {});
|
|
23
|
+
|
|
24
|
+
for (const ticker of tickers) {
|
|
25
|
+
const data = computed['asset-cost-basis-profile'][ticker];
|
|
26
|
+
if (!data) continue;
|
|
27
|
+
|
|
28
|
+
const current = data.current_price;
|
|
29
|
+
const totalInv = data.total_inventory_weight;
|
|
30
|
+
|
|
31
|
+
// Integrate Danger Zone (0% to -5% drop)
|
|
32
|
+
const riskVol = distribution.integrateProfile(data.profile, current * 0.95, current);
|
|
33
|
+
|
|
34
|
+
// Ratio: At-Risk Inventory / Total Inventory
|
|
35
|
+
const ratio = (totalInv > 0) ? (riskVol / totalInv) * 10 : 0;
|
|
36
|
+
|
|
37
|
+
let status = "STABLE";
|
|
38
|
+
if (ratio > 0.5) status = "FRAGILE";
|
|
39
|
+
if (ratio > 1.0) status = "CRITICAL_VACUUM";
|
|
40
|
+
|
|
41
|
+
this.vacuumResults[ticker] = {
|
|
42
|
+
crash_probability: Number(Math.min(ratio * 0.5, 0.99).toFixed(2)),
|
|
43
|
+
liquidity_ratio: Number(ratio.toFixed(4)),
|
|
44
|
+
status: status
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getResult() { return this.vacuumResults; }
|
|
50
|
+
reset() { this.vacuumResults = {}; }
|
|
51
|
+
}
|
|
52
|
+
module.exports = LiquidityVacuum;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Ghost Book: Retail Gamma Exposure
|
|
3
|
+
* Measures the "Elasticity of Intent" (Beta) between Price Change and Net Flow.
|
|
4
|
+
*/
|
|
5
|
+
class RetailGammaExposure {
|
|
6
|
+
constructor() { this.gammaResults = {}; }
|
|
7
|
+
|
|
8
|
+
static getMetadata() {
|
|
9
|
+
return {
|
|
10
|
+
type: 'meta',
|
|
11
|
+
dependencies: ['skilled-cohort-flow', 'instrument-price-change-1d'],
|
|
12
|
+
isHistorical: true, // Requires t-1, t-2... for rolling regression
|
|
13
|
+
category: 'ghost_book'
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static getDependencies() { return ['skilled-cohort-flow', 'instrument-price-change-1d']; }
|
|
18
|
+
|
|
19
|
+
static getSchema() {
|
|
20
|
+
return {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"patternProperties": {
|
|
23
|
+
"^.*$": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"properties": {
|
|
26
|
+
"gamma_beta": { "type": "number" },
|
|
27
|
+
"regime": { "type": "string", "enum": ["ACCELERANT", "STABILIZER", "NEUTRAL"] },
|
|
28
|
+
"_state": { "type": "object" } // Rolling buffer
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
process(context) {
|
|
36
|
+
const { computed, previousComputed, math } = context;
|
|
37
|
+
// FIX: Destructure correct keys from context.math
|
|
38
|
+
// 'signals' maps to SignalPrimitives, 'distribution' maps to DistributionAnalytics
|
|
39
|
+
const { signals, distribution } = math;
|
|
40
|
+
|
|
41
|
+
const tickers = signals.getUnionKeys(computed, ['skilled-cohort-flow']);
|
|
42
|
+
|
|
43
|
+
for (const ticker of tickers) {
|
|
44
|
+
// 1. Inputs
|
|
45
|
+
const flow = signals.getMetric(computed, 'skilled-cohort-flow', ticker, 'net_flow_pct', 0);
|
|
46
|
+
const priceChange = signals.getMetric(computed, 'instrument-price-change-1d', ticker, 'change_1d_pct', 0);
|
|
47
|
+
|
|
48
|
+
// 2. State Management (Rolling Window)
|
|
49
|
+
const prevResult = signals.getPreviousState(previousComputed, 'retail-gamma-exposure', ticker);
|
|
50
|
+
const bufferSize = 14; // 2-week regression window
|
|
51
|
+
|
|
52
|
+
let flowBuffer = prevResult?._state?.flow_buffer || [];
|
|
53
|
+
let priceBuffer = prevResult?._state?.price_buffer || [];
|
|
54
|
+
|
|
55
|
+
flowBuffer.push(flow);
|
|
56
|
+
priceBuffer.push(priceChange);
|
|
57
|
+
|
|
58
|
+
if (flowBuffer.length > bufferSize) {
|
|
59
|
+
flowBuffer.shift();
|
|
60
|
+
priceBuffer.shift();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 3. Regression: Flow ~ Alpha + Beta * PriceChange
|
|
64
|
+
const regression = distribution.linearRegression(priceBuffer, flowBuffer);
|
|
65
|
+
const beta = regression.slope;
|
|
66
|
+
|
|
67
|
+
// 4. Regime Logic
|
|
68
|
+
let regime = "NEUTRAL";
|
|
69
|
+
if (beta > 0.5) regime = "ACCELERANT"; // Volatility Explosion Watch
|
|
70
|
+
else if (beta < -0.5) regime = "STABILIZER"; // Chop Zone
|
|
71
|
+
|
|
72
|
+
this.gammaResults[ticker] = {
|
|
73
|
+
gamma_beta: Number(beta.toFixed(4)),
|
|
74
|
+
regime: regime,
|
|
75
|
+
_state: {
|
|
76
|
+
flow_buffer: flowBuffer,
|
|
77
|
+
price_buffer: priceBuffer
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getResult() { return this.gammaResults; }
|
|
84
|
+
reset() { this.gammaResults = {}; }
|
|
85
|
+
}
|
|
86
|
+
module.exports = RetailGammaExposure;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Cognitive Dissonance Arbitrage (CDA) v2.2
|
|
3
|
+
* REFACTORED: Adheres to System Architecture and Schema v1.
|
|
4
|
+
*/
|
|
5
|
+
class CognitiveDissonance {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.cdaResults = {};
|
|
8
|
+
this.alpha = 2 / (20 + 1); // EMA Alpha
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
static getMetadata() {
|
|
12
|
+
return {
|
|
13
|
+
type: 'meta', // Runs after standard calculations
|
|
14
|
+
rootDataDependencies: [],
|
|
15
|
+
isHistorical: true, // Requires t-1 state
|
|
16
|
+
userType: 'aggregate',
|
|
17
|
+
category: 'predictive_alpha'
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static getDependencies() {
|
|
22
|
+
return ['social-topic-sentiment-matrix', 'skilled-cohort-flow', 'instrument-price-change-1d'];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static getSchema() {
|
|
26
|
+
// Schema remains strictly compliant with user definition
|
|
27
|
+
const metricSchema = {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"properties": {
|
|
30
|
+
"cda_score": { "type": "number" },
|
|
31
|
+
"regime": { "type": "string" },
|
|
32
|
+
"z_sentiment": { "type": "number" },
|
|
33
|
+
"z_flow": { "type": "number" },
|
|
34
|
+
"price_confirmation": { "type": "boolean" },
|
|
35
|
+
"_state": { "type": "object" } // Opaque state object
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
return { "type": "object", "patternProperties": { "^.*$": metricSchema } };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
process(context) {
|
|
42
|
+
const { computed, previousComputed, math } = context;
|
|
43
|
+
const { signals: SignalPrimitives, TimeSeries } = math;
|
|
44
|
+
|
|
45
|
+
const tickers = SignalPrimitives.getUnionKeys(computed, CognitiveDissonance.getDependencies());
|
|
46
|
+
|
|
47
|
+
for (const ticker of tickers) {
|
|
48
|
+
// 1. Get Metrics (Safe Access)
|
|
49
|
+
// MAP: 'social-topic-sentiment-matrix' uses 'net_sentiment' NOT 'sentiment_score'
|
|
50
|
+
const rawSentiment = SignalPrimitives.getMetric(computed, 'social-topic-sentiment-matrix', ticker, 'net_sentiment', 0);
|
|
51
|
+
const rawFlow = SignalPrimitives.getMetric(computed, 'skilled-cohort-flow', ticker, 'net_flow_pct', 0);
|
|
52
|
+
const priceChange = SignalPrimitives.getMetric(computed, 'instrument-price-change-1d', ticker, 'change_1d_pct', 0); // Map to correct field
|
|
53
|
+
|
|
54
|
+
// 2. Get Previous State
|
|
55
|
+
const prevResult = SignalPrimitives.getPreviousState(previousComputed, 'cognitive-dissonance', ticker);
|
|
56
|
+
const prevState = prevResult ? prevResult._state : { sent_mean: 0, sent_var: 1, flow_mean: 0, flow_var: 1 };
|
|
57
|
+
|
|
58
|
+
// 3. Update Statistics (Math Layer)
|
|
59
|
+
const sentStats = TimeSeries.updateEMAState(rawSentiment, { mean: prevState.sent_mean, variance: prevState.sent_var }, this.alpha);
|
|
60
|
+
const flowStats = TimeSeries.updateEMAState(rawFlow, { mean: prevState.flow_mean, variance: prevState.flow_var }, this.alpha);
|
|
61
|
+
|
|
62
|
+
const sentStdDev = Math.sqrt(sentStats.variance);
|
|
63
|
+
const flowStdDev = Math.sqrt(flowStats.variance);
|
|
64
|
+
|
|
65
|
+
// 4. Compute Z-Scores
|
|
66
|
+
const zSentiment = (sentStdDev > 0.001) ? (rawSentiment - sentStats.mean) / sentStdDev : 0;
|
|
67
|
+
const zFlow = (flowStdDev > 0.001) ? (rawFlow - flowStats.mean) / flowStdDev : 0;
|
|
68
|
+
|
|
69
|
+
// 5. Logic (Voice vs Hands)
|
|
70
|
+
const interaction = zSentiment * zFlow;
|
|
71
|
+
let cdaScore = 0;
|
|
72
|
+
let regime = "NEUTRAL";
|
|
73
|
+
let priceConfirmation = false;
|
|
74
|
+
|
|
75
|
+
const disagree = (Math.sign(zSentiment) !== Math.sign(zFlow));
|
|
76
|
+
const loudVoice = Math.abs(zSentiment) > 1.65;
|
|
77
|
+
const activeHands = Math.abs(zFlow) > 0.5;
|
|
78
|
+
|
|
79
|
+
if (disagree && loudVoice && activeHands) {
|
|
80
|
+
cdaScore = -interaction; // Positive score = Bearish Dissonance (Euphoria + Selling)
|
|
81
|
+
|
|
82
|
+
const priceAgreesWithSentiment = (Math.sign(priceChange) === Math.sign(zSentiment));
|
|
83
|
+
const priceIsFlat = Math.abs(priceChange) < 0.5;
|
|
84
|
+
|
|
85
|
+
if (priceAgreesWithSentiment && !priceIsFlat) {
|
|
86
|
+
priceConfirmation = true;
|
|
87
|
+
regime = "CONVERGENT";
|
|
88
|
+
cdaScore = 0;
|
|
89
|
+
} else {
|
|
90
|
+
regime = (zSentiment > 0) ? "BEARISH_DISSONANCE" : "BULLISH_DISSONANCE";
|
|
91
|
+
}
|
|
92
|
+
} else if (!disagree && loudVoice) {
|
|
93
|
+
regime = "CONVERGENT";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.cdaResults[ticker] = {
|
|
97
|
+
cda_score: Number(cdaScore.toFixed(4)),
|
|
98
|
+
regime: regime,
|
|
99
|
+
z_sentiment: Number(zSentiment.toFixed(4)),
|
|
100
|
+
z_flow: Number(zFlow.toFixed(4)),
|
|
101
|
+
price_confirmation: priceConfirmation,
|
|
102
|
+
_state: {
|
|
103
|
+
sent_mean: sentStats.mean, sent_var: sentStats.variance,
|
|
104
|
+
flow_mean: flowStats.mean, flow_var: flowStats.variance
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getResult() { return this.cdaResults; }
|
|
111
|
+
reset() { this.cdaResults = {}; }
|
|
112
|
+
}
|
|
113
|
+
module.exports = CognitiveDissonance;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Diamond Hand Fracture Coefficient (DHFC) v2.2
|
|
3
|
+
* REFACTORED: Adheres to System Architecture.
|
|
4
|
+
*/
|
|
5
|
+
class DiamondHandFracture {
|
|
6
|
+
constructor() { this.fractureResults = {}; }
|
|
7
|
+
|
|
8
|
+
static getMetadata() {
|
|
9
|
+
return {
|
|
10
|
+
type: 'meta',
|
|
11
|
+
rootDataDependencies: [],
|
|
12
|
+
isHistorical: true,
|
|
13
|
+
userType: 'aggregate',
|
|
14
|
+
category: 'predictive_alpha'
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static getDependencies() {
|
|
19
|
+
return ['holding-duration-per-asset', 'average-daily-pnl-per-stock'];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static getSchema() {
|
|
23
|
+
const metricSchema = {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"properties": {
|
|
26
|
+
"fracture_velocity": { "type": "number" },
|
|
27
|
+
"capitulation_score": { "type": "number" },
|
|
28
|
+
"regime": { "type": "string" },
|
|
29
|
+
"expected_duration_hours": { "type": "number" },
|
|
30
|
+
"actual_duration_hours": { "type": "number" }
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
return { "type": "object", "patternProperties": { "^.*$": metricSchema } };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
process(context) {
|
|
37
|
+
const { computed, previousComputed, math } = context;
|
|
38
|
+
const { signals: SignalPrimitives } = math; // fixed?
|
|
39
|
+
|
|
40
|
+
const tickers = SignalPrimitives.getUnionKeys(computed, DiamondHandFracture.getDependencies());
|
|
41
|
+
|
|
42
|
+
for (const ticker of tickers) {
|
|
43
|
+
// 1. Current State
|
|
44
|
+
const H_t = SignalPrimitives.getMetric(computed, 'holding-duration-per-asset', ticker, 'avg_duration_hours', 0);
|
|
45
|
+
const OI_t = SignalPrimitives.getMetric(computed, 'holding-duration-per-asset', ticker, 'count', 0);
|
|
46
|
+
const avgPnl = SignalPrimitives.getMetric(computed, 'average-daily-pnl-per-stock', ticker, 'avg_daily_pnl_pct', 0);
|
|
47
|
+
|
|
48
|
+
// 2. Previous State (T-1)
|
|
49
|
+
// Note: We fetch 'holding-duration-per-asset' from *previous* computed results
|
|
50
|
+
const prevData = SignalPrimitives.getPreviousState(previousComputed, 'holding-duration-per-asset', ticker);
|
|
51
|
+
const H_prev = prevData ? (prevData.avg_duration_hours || 0) : 0;
|
|
52
|
+
const OI_prev = prevData ? (prevData.count || 0) : 0;
|
|
53
|
+
|
|
54
|
+
// Init Check
|
|
55
|
+
if (H_prev === 0 || OI_prev === 0) {
|
|
56
|
+
this.fractureResults[ticker] = { fracture_velocity: 0, capitulation_score: 0, regime: "STABLE", expected_duration_hours: H_t, actual_duration_hours: H_t };
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 3. Logic: Inventory Dilution
|
|
61
|
+
let dilutionFactor = (OI_t > 0) ? (OI_prev / OI_t) : 1.0;
|
|
62
|
+
if (dilutionFactor > 2.0) dilutionFactor = 1.0; // Clamp extreme dilution
|
|
63
|
+
|
|
64
|
+
const H_expected = H_prev * dilutionFactor;
|
|
65
|
+
const phi = H_expected - H_t;
|
|
66
|
+
const phi_normalized = (H_prev > 0) ? (phi / H_prev) * 100 : 0;
|
|
67
|
+
|
|
68
|
+
// 4. Capitulation Score
|
|
69
|
+
const painFactor = (avgPnl < 0) ? Math.abs(avgPnl) : 0;
|
|
70
|
+
const capitulationScore = phi_normalized * painFactor;
|
|
71
|
+
|
|
72
|
+
let regime = "STABLE";
|
|
73
|
+
if (phi_normalized > 5.0) regime = "FRACTURE";
|
|
74
|
+
if (capitulationScore > 20.0) regime = "CAPITULATION";
|
|
75
|
+
if (phi_normalized < -5.0) regime = "ACCUMULATION";
|
|
76
|
+
|
|
77
|
+
this.fractureResults[ticker] = {
|
|
78
|
+
fracture_velocity: Number(phi_normalized.toFixed(4)),
|
|
79
|
+
capitulation_score: Number(capitulationScore.toFixed(4)),
|
|
80
|
+
regime: regime,
|
|
81
|
+
expected_duration_hours: Number(H_expected.toFixed(2)),
|
|
82
|
+
actual_duration_hours: Number(H_t.toFixed(2))
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getResult() { return this.fractureResults; }
|
|
88
|
+
reset() { this.fractureResults = {}; }
|
|
89
|
+
}
|
|
90
|
+
module.exports = DiamondHandFracture;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Mimetic Latency Oscillator (MLO) v2.2
|
|
3
|
+
* REFACTORED: Offloaded math to primitives, corrected dependencies. TODO - HOW IS THIS GOING TO WORK GIVEN IT REQUIRES ITS OWN YESTERDAY DATA, BUT IT WILL BACKFILL IMMEDIATELY ON ITS FIRST PASS, AND THUS NOT FIND ANY YESTERDAY DATA? CONFUSED.
|
|
4
|
+
*/
|
|
5
|
+
class MimeticLatencyOscillator {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.mloResults = {};
|
|
8
|
+
this.windowSize = 30;
|
|
9
|
+
this.maxLag = 10;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static getMetadata() {
|
|
13
|
+
return {
|
|
14
|
+
type: 'meta',
|
|
15
|
+
rootDataDependencies: [],
|
|
16
|
+
isHistorical: true,
|
|
17
|
+
userType: 'aggregate',
|
|
18
|
+
category: 'predictive_alpha'
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static getDependencies() {
|
|
23
|
+
return ['skilled-cohort-flow', 'herd-consensus-score'];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static getSchema() {
|
|
27
|
+
const metricSchema = {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"properties": {
|
|
30
|
+
"lag_days": { "type": "integer" },
|
|
31
|
+
"correlation_strength": { "type": "number" },
|
|
32
|
+
"regime": { "type": "string" },
|
|
33
|
+
"_state": { "type": "object" }
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
return { "type": "object", "patternProperties": { "^.*$": metricSchema } };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
process(context) {
|
|
40
|
+
const { computed, previousComputed, math } = context;
|
|
41
|
+
const { signals: SignalPrimitives, TimeSeries } = math;
|
|
42
|
+
|
|
43
|
+
const tickers = SignalPrimitives.getUnionKeys(computed, MimeticLatencyOscillator.getDependencies());
|
|
44
|
+
|
|
45
|
+
for (const ticker of tickers) {
|
|
46
|
+
// 1. Inputs
|
|
47
|
+
// 'skilled-cohort-flow' -> net_flow_pct
|
|
48
|
+
const rawFlow = SignalPrimitives.getMetric(computed, 'skilled-cohort-flow', ticker, 'net_flow_pct', 0);
|
|
49
|
+
// 'herd-consensus-score' -> herd_conviction_score
|
|
50
|
+
const rawHerd = SignalPrimitives.getMetric(computed, 'herd-consensus-score', ticker, 'herd_conviction_score', 0);
|
|
51
|
+
|
|
52
|
+
// 2. Restore State
|
|
53
|
+
const prevResult = SignalPrimitives.getPreviousState(previousComputed, 'mimetic-latency', ticker);
|
|
54
|
+
const prevState = prevResult ? prevResult._state : { flow_buffer: [], herd_buffer: [], last_flow: 0, last_herd: 0 };
|
|
55
|
+
|
|
56
|
+
const prevFlow = (prevState.last_flow !== undefined) ? prevState.last_flow : 0;
|
|
57
|
+
const prevHerd = (prevState.last_herd !== undefined) ? prevState.last_herd : 0;
|
|
58
|
+
|
|
59
|
+
// 3. Calculate Detrended Delta
|
|
60
|
+
const flowDelta = rawFlow - prevFlow;
|
|
61
|
+
const herdDelta = rawHerd - prevHerd;
|
|
62
|
+
|
|
63
|
+
// 4. Update Buffers
|
|
64
|
+
let flowBuffer = [...(prevState.flow_buffer || [])];
|
|
65
|
+
let herdBuffer = [...(prevState.herd_buffer || [])];
|
|
66
|
+
|
|
67
|
+
flowBuffer.push(flowDelta);
|
|
68
|
+
herdBuffer.push(herdDelta);
|
|
69
|
+
|
|
70
|
+
if (flowBuffer.length > this.windowSize) flowBuffer.shift();
|
|
71
|
+
if (herdBuffer.length > this.windowSize) herdBuffer.shift();
|
|
72
|
+
|
|
73
|
+
// 5. Lagged Cross-Correlation
|
|
74
|
+
let maxCorr = -1.0;
|
|
75
|
+
let bestLag = 0;
|
|
76
|
+
|
|
77
|
+
if (flowBuffer.length >= 15) {
|
|
78
|
+
for (let k = 0; k <= this.maxLag; k++) {
|
|
79
|
+
// Check if Flow[t-k] predicts Herd[t]
|
|
80
|
+
// Slice Flow: 0 to End-k
|
|
81
|
+
// Slice Herd: k to End
|
|
82
|
+
const len = flowBuffer.length;
|
|
83
|
+
if (len - k < 5) continue; // Min sample check
|
|
84
|
+
|
|
85
|
+
const slicedFlow = flowBuffer.slice(0, len - k);
|
|
86
|
+
const slicedHerd = herdBuffer.slice(k, len);
|
|
87
|
+
|
|
88
|
+
const r = TimeSeries.pearsonCorrelation(slicedFlow, slicedHerd);
|
|
89
|
+
|
|
90
|
+
if (r > maxCorr) {
|
|
91
|
+
maxCorr = r;
|
|
92
|
+
bestLag = k;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 6. Regime
|
|
98
|
+
let regime = "NO_SIGNAL";
|
|
99
|
+
if (maxCorr > 0.3) {
|
|
100
|
+
if (bestLag >= 3) regime = "EARLY_ALPHA";
|
|
101
|
+
else if (bestLag >= 1) regime = "MARKUP";
|
|
102
|
+
else regime = "FOMO_TRAP";
|
|
103
|
+
} else {
|
|
104
|
+
regime = "DECOUPLING";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
this.mloResults[ticker] = {
|
|
108
|
+
lag_days: bestLag,
|
|
109
|
+
correlation_strength: Number(maxCorr.toFixed(4)),
|
|
110
|
+
regime: regime,
|
|
111
|
+
_state: {
|
|
112
|
+
flow_buffer: flowBuffer,
|
|
113
|
+
herd_buffer: herdBuffer,
|
|
114
|
+
last_flow: rawFlow,
|
|
115
|
+
last_herd: rawHerd
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
getResult() { return this.mloResults; }
|
|
122
|
+
reset() { this.mloResults = {}; }
|
|
123
|
+
}
|
|
124
|
+
module.exports = MimeticLatencyOscillator;
|