aiden-shared-calculations-unified 1.0.95 → 1.0.96
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 +1 -2
- package/calculations/capitulation/retail-capitulation-risk-forecast.js +1 -2
- package/calculations/ghost-book/cost-basis-density.js +1 -2
- package/calculations/ghost-book/liquidity-vacuum.js +1 -2
- package/calculations/ghost-book/retail-gamma-exposure.js +0 -1
- package/calculations/predicative-alpha/cognitive-dissonance.js +1 -2
- package/calculations/predicative-alpha/diamond-hand-fracture.js +1 -2
- package/calculations/predicative-alpha/mimetic-latency.js +1 -2
- package/package.json +1 -1
- package/calculations/legacy/activity_by_pnl_status.js +0 -119
- package/calculations/legacy/asset_crowd_flow.js +0 -163
- package/calculations/legacy/capital_deployment_strategy.js +0 -108
- package/calculations/legacy/capital_liquidation_performance.js +0 -139
- package/calculations/legacy/capital_vintage_performance.js +0 -136
- package/calculations/legacy/cash-flow-deployment.js +0 -144
- package/calculations/legacy/cash-flow-liquidation.js +0 -146
- package/calculations/legacy/crowd-cash-flow-proxy.js +0 -128
- package/calculations/legacy/crowd_conviction_score.js +0 -261
- package/calculations/legacy/crowd_sharpe_ratio_proxy.js +0 -137
- package/calculations/legacy/daily_asset_activity.js +0 -128
- package/calculations/legacy/daily_user_activity_tracker.js +0 -182
- package/calculations/legacy/deposit_withdrawal_percentage.js +0 -125
- package/calculations/legacy/diversification_pnl.js +0 -115
- package/calculations/legacy/drawdown_response.js +0 -137
- package/calculations/legacy/dumb-cohort-flow.js +0 -238
- package/calculations/legacy/gain_response.js +0 -137
- package/calculations/legacy/historical_performance_aggregator.js +0 -85
- package/calculations/legacy/in_loss_asset_crowd_flow.js +0 -168
- package/calculations/legacy/in_profit_asset_crowd_flow.js +0 -168
- package/calculations/legacy/negative_expectancy_cohort_flow.js +0 -232
- package/calculations/legacy/new_allocation_percentage.js +0 -98
- package/calculations/legacy/paper_vs_diamond_hands.js +0 -107
- package/calculations/legacy/position_count_pnl.js +0 -120
- package/calculations/legacy/positive_expectancy_cohort_flow.js +0 -232
- package/calculations/legacy/profit_cohort_divergence.js +0 -115
- package/calculations/legacy/profitability_migration.js +0 -104
- package/calculations/legacy/reallocation_increase_percentage.js +0 -104
- package/calculations/legacy/risk_appetite_change.js +0 -97
- package/calculations/legacy/sector_rotation.js +0 -117
- package/calculations/legacy/shark_attack_signal.js +0 -112
- package/calculations/legacy/smart-cohort-flow.js +0 -238
- package/calculations/legacy/smart-dumb-divergence-index.js +0 -143
- package/calculations/legacy/smart_dumb_divergence_index_v2.js +0 -138
- package/calculations/legacy/smart_money_flow.js +0 -198
- package/calculations/legacy/social-predictive-regime-state.js +0 -102
- package/calculations/legacy/social-topic-driver-index.js +0 -147
- package/calculations/legacy/social-topic-predictive-potential.js +0 -461
- package/calculations/legacy/social_flow_correlation.js +0 -112
- package/calculations/legacy/speculator_adjustment_activity.js +0 -103
- package/calculations/legacy/strategy-performance.js +0 -265
- package/calculations/legacy/tsl_effectiveness.js +0 -85
- package/calculations/legacy/user-investment-profile.js +0 -313
- package/calculations/legacy/user_expectancy_score.js +0 -106
- package/calculations/legacy/user_profitability_tracker.js +0 -131
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Calculation (Pass 1) for speculator metric.
|
|
3
|
-
*
|
|
4
|
-
* This metric tracks the change in a speculator's risk appetite by
|
|
5
|
-
* comparing the "risk" of their portfolio today vs. yesterday.
|
|
6
|
-
* Risk is defined as (TotalLeveragedInvestment / TotalInvestment).
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
class RiskAppetiteChange {
|
|
10
|
-
constructor() {
|
|
11
|
-
// We will store { [userId]: { risk_appetite_change: 0 } }
|
|
12
|
-
this.userRiskMap = new Map();
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Defines the output schema for this calculation.
|
|
17
|
-
* @returns {object} JSON Schema object
|
|
18
|
-
*/
|
|
19
|
-
static getSchema() {
|
|
20
|
-
return {
|
|
21
|
-
"type": "object",
|
|
22
|
-
"description": "Tracks the daily change in risk appetite (leveraged investment / total investment) for each speculator.",
|
|
23
|
-
"patternProperties": {
|
|
24
|
-
"^[a-zA-Z0-9_]+$": { // Matches any user ID
|
|
25
|
-
"type": "object",
|
|
26
|
-
"properties": {
|
|
27
|
-
"risk_appetite_change": {
|
|
28
|
-
"type": "number",
|
|
29
|
-
"description": "The percentage point change in risk appetite from yesterday to today."
|
|
30
|
-
}
|
|
31
|
-
},
|
|
32
|
-
"required": ["risk_appetite_change"]
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
"additionalProperties": false
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Helper to calculate the risk score from a portfolio snapshot.
|
|
41
|
-
* @param {object} portfolio - The portfolio snapshot.
|
|
42
|
-
* @returns {number} The risk score (0-100).
|
|
43
|
-
*/
|
|
44
|
-
_calculateRisk(portfolio) {
|
|
45
|
-
const totalInvestment = portfolio?.TotalInvestment;
|
|
46
|
-
const totalLeveragedInvestment = portfolio?.TotalLeveragedInvestment;
|
|
47
|
-
|
|
48
|
-
if (!totalInvestment || totalInvestment === 0 || !totalLeveragedInvestment) {
|
|
49
|
-
return 0;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Return as a percentage
|
|
53
|
-
return (totalLeveragedInvestment / totalInvestment) * 100;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* @param {object} todayPortfolio
|
|
58
|
-
* @param {object} yesterdayPortfolio
|
|
59
|
-
* @param {string} userId
|
|
60
|
-
*/
|
|
61
|
-
process(todayPortfolio, yesterdayPortfolio, userId) {
|
|
62
|
-
// --- FIX ---
|
|
63
|
-
// Add null check to prevent crash on the first day of processing
|
|
64
|
-
if (!todayPortfolio || !yesterdayPortfolio || !todayPortfolio.Portfolio || !yesterdayPortfolio.Portfolio) {
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// --- END FIX ---
|
|
70
|
-
|
|
71
|
-
// 1. Get portfolio snapshots
|
|
72
|
-
const yPortfolio = yesterdayPortfolio.Portfolio;
|
|
73
|
-
const tPortfolio = todayPortfolio.Portfolio;
|
|
74
|
-
|
|
75
|
-
// 2. Calculate risk for both days
|
|
76
|
-
const yRisk = this._calculateRisk(yPortfolio);
|
|
77
|
-
const tRisk = this._calculateRisk(tPortfolio);
|
|
78
|
-
|
|
79
|
-
// 3. Calculate the change (in percentage points)
|
|
80
|
-
const change = tRisk - yRisk;
|
|
81
|
-
|
|
82
|
-
this.userRiskMap.set(userId, {
|
|
83
|
-
risk_appetite_change: change
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
getResult() {
|
|
88
|
-
// Convert Map to plain object for Firestore
|
|
89
|
-
return Object.fromEntries(this.userRiskMap);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
reset() {
|
|
93
|
-
this.userRiskMap.clear();
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
module.exports = RiskAppetiteChange;
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Calculation (Pass 2) for sector rotation.
|
|
3
|
-
*
|
|
4
|
-
* This metric answers: "What is the net change in investment
|
|
5
|
-
* (as a portfolio percentage) for each sector today?"
|
|
6
|
-
*
|
|
7
|
-
* This measures which sectors the crowd is rotating into or out of.
|
|
8
|
-
*/
|
|
9
|
-
const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
|
|
10
|
-
|
|
11
|
-
class SectorRotation {
|
|
12
|
-
constructor() {
|
|
13
|
-
// We will store { [sector]: { y_invested_sum: 0, t_invested_sum: 0 } }
|
|
14
|
-
this.sectors = new Map();
|
|
15
|
-
this.mappings = null;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Defines the output schema for this calculation.
|
|
20
|
-
* @returns {object} JSON Schema object
|
|
21
|
-
*/
|
|
22
|
-
static getSchema() {
|
|
23
|
-
const sectorSchema = {
|
|
24
|
-
"type": "object",
|
|
25
|
-
"description": "Net change in portfolio allocation for a specific sector.",
|
|
26
|
-
"properties": {
|
|
27
|
-
"net_change_pct": {
|
|
28
|
-
"type": "number",
|
|
29
|
-
"description": "The net change in percentage allocation (Today Sum - Yesterday Sum)."
|
|
30
|
-
},
|
|
31
|
-
"y_total_invested_pct": {
|
|
32
|
-
"type": "number",
|
|
33
|
-
"description": "Total portfolio percentage invested in this sector yesterday."
|
|
34
|
-
},
|
|
35
|
-
"t_total_invested_pct": {
|
|
36
|
-
"type": "number",
|
|
37
|
-
"description": "Total portfolio percentage invested in this sector today."
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
"required": ["net_change_pct", "y_total_invested_pct", "t_total_invested_pct"]
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
return {
|
|
44
|
-
"type": "object",
|
|
45
|
-
"description": "Calculates the net rotation of portfolio percentage allocation between sectors.",
|
|
46
|
-
"patternProperties": {
|
|
47
|
-
"^.*$": sectorSchema // Sector name
|
|
48
|
-
},
|
|
49
|
-
"additionalProperties": sectorSchema
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
_initSector(sector) {
|
|
54
|
-
if (!this.sectors.has(sector)) {
|
|
55
|
-
this.sectors.set(sector, {
|
|
56
|
-
y_invested_sum: 0,
|
|
57
|
-
t_invested_sum: 0
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
_sumSectorAllocations(portfolio, mapKey) {
|
|
63
|
-
const positions = portfolio?.AggregatedPositions; // Must use Aggregated for 'Invested' %
|
|
64
|
-
if (!positions || !Array.isArray(positions) || !this.mappings) {
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
for (const pos of positions) {
|
|
69
|
-
const instrumentId = pos.InstrumentID;
|
|
70
|
-
if (instrumentId) {
|
|
71
|
-
const sector = this.mappings.instrumentToSector[instrumentId] || 'Other';
|
|
72
|
-
this._initSector(sector);
|
|
73
|
-
|
|
74
|
-
const investedPct = pos.InvestedAmount || pos.Invested || 0;
|
|
75
|
-
this.sectors.get(sector)[mapKey] += investedPct;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
process(todayPortfolio, yesterdayPortfolio, userId, context) {
|
|
81
|
-
if (!todayPortfolio || !yesterdayPortfolio) {
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (!this.mappings) {
|
|
86
|
-
// Context contains the mappings loaded in Pass 1
|
|
87
|
-
this.mappings = context.mappings;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
this._sumSectorAllocations(yesterdayPortfolio, 'y_invested_sum');
|
|
91
|
-
this._sumSectorAllocations(todayPortfolio, 't_invested_sum');
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
getResult() {
|
|
95
|
-
const result = {};
|
|
96
|
-
for (const [sector, data] of this.sectors.entries()) {
|
|
97
|
-
const netChange = data.t_invested_sum - data.y_invested_sum;
|
|
98
|
-
|
|
99
|
-
// Only report sectors that had some activity
|
|
100
|
-
if (data.t_invested_sum > 0 || data.y_invested_sum > 0) {
|
|
101
|
-
result[sector] = {
|
|
102
|
-
net_change_pct: netChange,
|
|
103
|
-
y_total_invested_pct: data.y_invested_sum,
|
|
104
|
-
t_total_invested_pct: data.t_invested_sum
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return result;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
reset() {
|
|
112
|
-
this.sectors.clear();
|
|
113
|
-
this.mappings = null;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
module.exports = SectorRotation;
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Calculation (Pass 4) for "Shark Attack" signal.
|
|
3
|
-
*
|
|
4
|
-
* This signal identifies when the "Smart Cohort" is behaving
|
|
5
|
-
* differently from both the "Dumb Cohort" and the "In-Loss Cohort"
|
|
6
|
-
* simultaneously.
|
|
7
|
-
*
|
|
8
|
-
* e.g., Smart buying, while Dumb AND Loss cohorts are selling.
|
|
9
|
-
*
|
|
10
|
-
* It *depends* on 'smart-cohort-flow', 'dumb-cohort-flow',
|
|
11
|
-
* and 'in_loss_asset_crowd_flow'.
|
|
12
|
-
*/
|
|
13
|
-
class SharkAttackSignal {
|
|
14
|
-
constructor() {
|
|
15
|
-
// No per-user processing
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Defines the output schema for this calculation.
|
|
20
|
-
* @returns {object} JSON Schema object
|
|
21
|
-
*/
|
|
22
|
-
static getSchema() {
|
|
23
|
-
const signalSchema = {
|
|
24
|
-
"type": "object",
|
|
25
|
-
"properties": {
|
|
26
|
-
"status": {
|
|
27
|
-
"type": "string",
|
|
28
|
-
"enum": ["Shark Attack (Buy)", "Shark Bait (Sell)", "Neutral"]
|
|
29
|
-
},
|
|
30
|
-
"smart_flow_pct": { "type": "number" },
|
|
31
|
-
"dumb_flow_pct": { "type": "number" },
|
|
32
|
-
"loss_cohort_flow_pct": { "type": "number" }
|
|
33
|
-
},
|
|
34
|
-
"required": ["status", "smart_flow_pct", "dumb_flow_pct", "loss_cohort_flow_pct"]
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
return {
|
|
38
|
-
"type": "object",
|
|
39
|
-
"description": "Generates 'Shark Attack' signals where Smart Cohort flow diverges from both Dumb and In-Loss cohorts.",
|
|
40
|
-
"patternProperties": {
|
|
41
|
-
"^.*$": signalSchema // Ticker
|
|
42
|
-
},
|
|
43
|
-
"additionalProperties": signalSchema
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Statically declare dependencies.
|
|
49
|
-
*/
|
|
50
|
-
static getDependencies() {
|
|
51
|
-
return [
|
|
52
|
-
'smart-cohort-flow', // Pass 3
|
|
53
|
-
'dumb-cohort-flow', // Pass 3
|
|
54
|
-
'in_loss_asset_crowd_flow'// Pass 3
|
|
55
|
-
];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
process() {
|
|
59
|
-
// No-op
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
getResult(fetchedDependencies) {
|
|
63
|
-
const smartFlowData = fetchedDependencies['smart-cohort-flow']?.assets;
|
|
64
|
-
const dumbFlowData = fetchedDependencies['dumb-cohort-flow']?.assets;
|
|
65
|
-
const lossFlowData = fetchedDependencies['in_loss_asset_crowd_flow'];
|
|
66
|
-
|
|
67
|
-
if (!smartFlowData || !dumbFlowData || !lossFlowData) {
|
|
68
|
-
return {};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const allTickers = new Set([
|
|
72
|
-
...Object.keys(smartFlowData),
|
|
73
|
-
...Object.keys(dumbFlowData),
|
|
74
|
-
...Object.keys(lossFlowData)
|
|
75
|
-
]);
|
|
76
|
-
|
|
77
|
-
const result = {};
|
|
78
|
-
const THRESHOLD = 1.5; // Higher threshold for a stronger signal
|
|
79
|
-
|
|
80
|
-
for (const ticker of allTickers) {
|
|
81
|
-
const sFlow = smartFlowData[ticker]?.net_flow_percentage || 0;
|
|
82
|
-
const dFlow = dumbFlowData[ticker]?.net_flow_percentage || 0;
|
|
83
|
-
const lFlow = lossFlowData[ticker]?.net_flow_percentage || 0;
|
|
84
|
-
|
|
85
|
-
let status = 'Neutral';
|
|
86
|
-
|
|
87
|
-
// "Shark Attack": Smart buying, Dumb selling, Loss selling
|
|
88
|
-
if (sFlow > THRESHOLD && dFlow < -THRESHOLD && lFlow < -THRESHOLD) {
|
|
89
|
-
status = 'Shark Attack (Buy)';
|
|
90
|
-
}
|
|
91
|
-
// "Shark Bait": Smart selling, Dumb buying, Loss buying
|
|
92
|
-
else if (sFlow < -THRESHOLD && dFlow > THRESHOLD && lFlow > THRESHOLD) {
|
|
93
|
-
status = 'Shark Bait (Sell)';
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
result[ticker] = {
|
|
97
|
-
status: status,
|
|
98
|
-
smart_flow_pct: sFlow,
|
|
99
|
-
dumb_flow_pct: dFlow,
|
|
100
|
-
loss_cohort_flow_pct: lFlow
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return result;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
reset() {
|
|
108
|
-
// No state
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
module.exports = SharkAttackSignal;
|
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Calculation (Pass 3) for smart cohort flow.
|
|
3
|
-
*
|
|
4
|
-
* This metric calculates the "Net Crowd Flow Percentage" for the
|
|
5
|
-
* "Smart Cohort" (top 20% of investors).
|
|
6
|
-
*
|
|
7
|
-
* This calculation *depends* on 'user_profitability_tracker'
|
|
8
|
-
* to identify the cohort.
|
|
9
|
-
*/
|
|
10
|
-
const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
|
|
11
|
-
|
|
12
|
-
class SmartCohortFlow {
|
|
13
|
-
constructor() {
|
|
14
|
-
this.assetData = new Map();
|
|
15
|
-
this.sectorData = new Map();
|
|
16
|
-
this.mappings = null;
|
|
17
|
-
this.smartCohortUserIds = null;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Defines the output schema for this calculation.
|
|
22
|
-
* @returns {object} JSON Schema object
|
|
23
|
-
*/
|
|
24
|
-
static getSchema() {
|
|
25
|
-
const flowSchema = {
|
|
26
|
-
"type": "object",
|
|
27
|
-
"properties": {
|
|
28
|
-
"net_flow_percentage": { "type": "number" },
|
|
29
|
-
"total_invested_today": { "type": "number" },
|
|
30
|
-
"total_invested_yesterday": { "type": "number" }
|
|
31
|
-
},
|
|
32
|
-
"required": ["net_flow_percentage", "total_invested_today", "total_invested_yesterday"]
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
return {
|
|
36
|
-
"type": "object",
|
|
37
|
-
"description": "Calculates net capital flow % (price-adjusted) for the 'Smart Cohort' (top 20% users), aggregated by asset and sector.",
|
|
38
|
-
"properties": {
|
|
39
|
-
"cohort_size": {
|
|
40
|
-
"type": "number",
|
|
41
|
-
"description": "The number of users identified as being in the Smart Cohort."
|
|
42
|
-
},
|
|
43
|
-
"assets": {
|
|
44
|
-
"type": "object",
|
|
45
|
-
"description": "Price-adjusted net flow per asset.",
|
|
46
|
-
"patternProperties": { "^.*$": flowSchema }, // Ticker
|
|
47
|
-
"additionalProperties": flowSchema
|
|
48
|
-
},
|
|
49
|
-
"sectors": {
|
|
50
|
-
"type": "object",
|
|
51
|
-
"description": "Price-adjusted net flow per sector.",
|
|
52
|
-
"patternProperties": { "^.*$": flowSchema }, // Sector
|
|
53
|
-
"additionalProperties": flowSchema
|
|
54
|
-
}
|
|
55
|
-
},
|
|
56
|
-
"required": ["cohort_size", "assets", "sectors"]
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Statically declare dependencies.
|
|
62
|
-
*/
|
|
63
|
-
static getDependencies() {
|
|
64
|
-
return ['user_profitability_tracker'];
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
_getPortfolioPositions(portfolio) {
|
|
68
|
-
return portfolio?.PublicPositions || portfolio?.AggregatedPositions;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
_initAsset(instrumentId) {
|
|
72
|
-
if (!this.assetData.has(instrumentId)) {
|
|
73
|
-
this.assetData.set(instrumentId, {
|
|
74
|
-
total_invested_yesterday: 0,
|
|
75
|
-
total_invested_today: 0,
|
|
76
|
-
price_change_yesterday: 0,
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
_initSector(sector) {
|
|
82
|
-
if (!this.sectorData.has(sector)) {
|
|
83
|
-
this.sectorData.set(sector, {
|
|
84
|
-
total_invested_yesterday: 0,
|
|
85
|
-
total_invested_today: 0,
|
|
86
|
-
price_change_yesterday: 0,
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Helper to get the cohort IDs from the dependency.
|
|
93
|
-
*/
|
|
94
|
-
_getSmartCohort(fetchedDependencies) {
|
|
95
|
-
if (this.smartCohortUserIds) {
|
|
96
|
-
return this.smartCohortUserIds;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const profitabilityData = fetchedDependencies['user_profitability_tracker'];
|
|
100
|
-
if (!profitabilityData || !profitabilityData.ranked_users) {
|
|
101
|
-
return new Set();
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const rankedUsers = profitabilityData.ranked_users;
|
|
105
|
-
const cohortSize = Math.floor(rankedUsers.length * 0.20);
|
|
106
|
-
|
|
107
|
-
// The 'ranked_users' are sorted ascending by P&L, so top 20% is the *last* 20%.
|
|
108
|
-
this.smartCohortUserIds = new Set(rankedUsers.slice(-cohortSize).map(u => u.userId));
|
|
109
|
-
return this.smartCohortUserIds;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights, fetchedDependencies) {
|
|
113
|
-
const smartCohort = this._getSmartCohort(fetchedDependencies);
|
|
114
|
-
|
|
115
|
-
// This user is not in the "smart cohort", skip.
|
|
116
|
-
if (!smartCohort.has(userId)) {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (!todayPortfolio || !yesterdayPortfolio) {
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const yPos = this._getPortfolioPositions(yesterdayPortfolio);
|
|
125
|
-
const tPos = this._getPortfolioPositions(todayPortfolio);
|
|
126
|
-
|
|
127
|
-
const yPosMap = new Map(yPos?.map(p => [p.InstrumentID, p]) || []);
|
|
128
|
-
const tPosMap = new Map(tPos?.map(p => [p.InstrumentID, p]) || []);
|
|
129
|
-
|
|
130
|
-
const allInstrumentIds = new Set([...yPosMap.keys(), ...tPosMap.keys()]);
|
|
131
|
-
|
|
132
|
-
if (!this.mappings) {
|
|
133
|
-
// Context contains the mappings loaded in Pass 1
|
|
134
|
-
this.mappings = context.mappings;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
for (const instrumentId of allInstrumentIds) {
|
|
138
|
-
if (!instrumentId) continue;
|
|
139
|
-
|
|
140
|
-
this._initAsset(instrumentId);
|
|
141
|
-
const asset = this.assetData.get(instrumentId);
|
|
142
|
-
|
|
143
|
-
const yP = yPosMap.get(instrumentId);
|
|
144
|
-
const tP = tPosMap.get(instrumentId);
|
|
145
|
-
|
|
146
|
-
const yInvested = yP?.InvestedAmount || yP?.Amount || 0;
|
|
147
|
-
const tInvested = tP?.InvestedAmount || tP?.Amount || 0;
|
|
148
|
-
|
|
149
|
-
// Get sector and initialize it
|
|
150
|
-
const sector = this.mappings.instrumentToSector[instrumentId] || 'Other';
|
|
151
|
-
this._initSector(sector);
|
|
152
|
-
const sectorAsset = this.sectorData.get(sector);
|
|
153
|
-
|
|
154
|
-
if (yInvested > 0) {
|
|
155
|
-
const yPriceChange = (yP?.PipsRate || 0) / (yP?.OpenRate || 1);
|
|
156
|
-
|
|
157
|
-
asset.total_invested_yesterday += yInvested;
|
|
158
|
-
asset.price_change_yesterday += yPriceChange * yInvested;
|
|
159
|
-
|
|
160
|
-
sectorAsset.total_invested_yesterday += yInvested;
|
|
161
|
-
sectorAsset.price_change_yesterday += yPriceChange * yInvested;
|
|
162
|
-
}
|
|
163
|
-
if (tInvested > 0) {
|
|
164
|
-
asset.total_invested_today += tInvested;
|
|
165
|
-
sectorAsset.total_invested_today += tInvested;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
_calculateFlow(dataMap) {
|
|
171
|
-
const result = {};
|
|
172
|
-
for (const [key, data] of dataMap.entries()) {
|
|
173
|
-
const { total_invested_yesterday, total_invested_today, price_change_yesterday } = data;
|
|
174
|
-
|
|
175
|
-
if (total_invested_yesterday > 0) {
|
|
176
|
-
const avg_price_change_pct = price_change_yesterday / total_invested_yesterday;
|
|
177
|
-
const price_contribution = total_invested_yesterday * avg_price_change_pct;
|
|
178
|
-
const flow_contribution = total_invested_today - (total_invested_yesterday + price_contribution);
|
|
179
|
-
const net_flow_percentage = (flow_contribution / total_invested_yesterday) * 100;
|
|
180
|
-
|
|
181
|
-
result[key] = {
|
|
182
|
-
net_flow_percentage: net_flow_percentage,
|
|
183
|
-
total_invested_today: total_invested_today,
|
|
184
|
-
total_invested_yesterday: total_invested_yesterday
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
return result;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async getResult(fetchedDependencies) {
|
|
192
|
-
// Ensure mappings are loaded (can be from context or loaded now)
|
|
193
|
-
if (!this.mappings) {
|
|
194
|
-
this.mappings = await loadInstrumentMappings();
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Ensure cohort is calculated at least once
|
|
198
|
-
const smartCohort = this._getSmartCohort(fetchedDependencies);
|
|
199
|
-
|
|
200
|
-
// 1. Calculate Asset Flow
|
|
201
|
-
const assetResult = {};
|
|
202
|
-
for (const [instrumentId, data] of this.assetData.entries()) {
|
|
203
|
-
const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
|
|
204
|
-
const { total_invested_yesterday, total_invested_today, price_change_yesterday } = data;
|
|
205
|
-
|
|
206
|
-
if (total_invested_yesterday > 0) {
|
|
207
|
-
const avg_price_change_pct = price_change_yesterday / total_invested_yesterday;
|
|
208
|
-
const price_contribution = total_invested_yesterday * avg_price_change_pct;
|
|
209
|
-
const flow_contribution = total_invested_today - (total_invested_yesterday + price_contribution);
|
|
210
|
-
const net_flow_percentage = (flow_contribution / total_invested_yesterday) * 100;
|
|
211
|
-
|
|
212
|
-
assetResult[ticker] = {
|
|
213
|
-
net_flow_percentage: net_flow_percentage,
|
|
214
|
-
total_invested_today: total_invested_today,
|
|
215
|
-
total_invested_yesterday: total_invested_yesterday
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// 2. Calculate Sector Flow
|
|
221
|
-
const sectorResult = this._calculateFlow(this.sectorData);
|
|
222
|
-
|
|
223
|
-
return {
|
|
224
|
-
cohort_size: smartCohort.size,
|
|
225
|
-
assets: assetResult,
|
|
226
|
-
sectors: sectorResult
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
reset() {
|
|
231
|
-
this.assetData.clear();
|
|
232
|
-
this.sectorData.clear();
|
|
233
|
-
this.mappings = null;
|
|
234
|
-
this.smartCohortUserIds = null;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
module.exports = SmartCohortFlow;
|