aiden-shared-calculations-unified 1.0.23 → 1.0.25
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/activity/historical/activity_by_pnl_status.js +86 -0
- package/calculations/activity/historical/daily_asset_activity.js +86 -0
- package/calculations/activity/historical/speculator_adjustment_activity.js +77 -0
- package/calculations/meta/capital_vintage_performance.js +159 -0
- package/package.json +1 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Analyzes *why* users were active by checking the P&L status
|
|
3
|
+
* of positions just before they were closed.
|
|
4
|
+
* This measures "Profit Taking" vs. "Capitulation".
|
|
5
|
+
*/
|
|
6
|
+
class ActivityByPnlStatus {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.total_positions_yesterday = {
|
|
9
|
+
in_profit: 0,
|
|
10
|
+
in_loss: 0
|
|
11
|
+
};
|
|
12
|
+
this.closed_positions_today = {
|
|
13
|
+
profit_taken: 0,
|
|
14
|
+
loss_realized: 0
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_getPortfolioMap(portfolio) {
|
|
19
|
+
// We MUST use PositionID here to track specific trades, not just the asset
|
|
20
|
+
const positions = portfolio?.AggregatedPositions || portfolio?.PublicPositions;
|
|
21
|
+
if (!positions || !Array.isArray(positions)) {
|
|
22
|
+
return new Map();
|
|
23
|
+
}
|
|
24
|
+
// Map<PositionID, NetProfit>
|
|
25
|
+
return new Map(positions.map(p => [p.PositionID, p.NetProfit || 0]));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
process(todayPortfolio, yesterdayPortfolio, userId) {
|
|
29
|
+
if (!todayPortfolio || !yesterdayPortfolio) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const yPosMap = this._getPortfolioMap(yesterdayPortfolio);
|
|
34
|
+
const tPosMap = this._getPortfolioMap(todayPortfolio);
|
|
35
|
+
|
|
36
|
+
if (yPosMap.size === 0) {
|
|
37
|
+
return; // No positions yesterday to analyze
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const [yPosId, yNetProfit] of yPosMap.entries()) {
|
|
41
|
+
// 1. Bucket yesterday's P&L state
|
|
42
|
+
if (yNetProfit > 0) {
|
|
43
|
+
this.total_positions_yesterday.in_profit++;
|
|
44
|
+
} else if (yNetProfit < 0) {
|
|
45
|
+
this.total_positions_yesterday.in_loss++;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 2. Check if this position was closed
|
|
49
|
+
if (!tPosMap.has(yPosId)) {
|
|
50
|
+
// Position was closed. Check its P&L from yesterday.
|
|
51
|
+
if (yNetProfit > 0) {
|
|
52
|
+
this.closed_positions_today.profit_taken++;
|
|
53
|
+
} else if (yNetProfit < 0) {
|
|
54
|
+
this.closed_positions_today.loss_realized++;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getResult() {
|
|
61
|
+
const { in_profit, in_loss } = this.total_positions_yesterday;
|
|
62
|
+
const { profit_taken, loss_realized } = this.closed_positions_today;
|
|
63
|
+
|
|
64
|
+
// Calculate rates to normalize the data
|
|
65
|
+
const profit_taking_rate = (in_profit > 0) ? (profit_taken / in_profit) * 100 : 0;
|
|
66
|
+
const capitulation_rate = (in_loss > 0) ? (loss_realized / in_loss) * 100 : 0;
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
profit_taking_rate_pct: profit_taking_rate, // % of profitable positions that were closed
|
|
70
|
+
capitulation_rate_pct: capitulation_rate, // % of losing positions that were closed
|
|
71
|
+
raw_counts: {
|
|
72
|
+
profit_positions_closed: profit_taken,
|
|
73
|
+
loss_positions_closed: loss_realized,
|
|
74
|
+
total_profit_positions_available: in_profit,
|
|
75
|
+
total_loss_positions_available: in_loss
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
reset() {
|
|
81
|
+
this.total_positions_yesterday = { in_profit: 0, in_loss: 0 };
|
|
82
|
+
this.closed_positions_today = { profit_taken: 0, loss_realized: 0 };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = ActivityByPnlStatus;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tracks the flow of unique users opening or closing positions
|
|
3
|
+
* on a per-asset basis. This measures the "focus" of the crowd's activity.
|
|
4
|
+
*/
|
|
5
|
+
const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
|
|
6
|
+
|
|
7
|
+
class DailyAssetActivity {
|
|
8
|
+
constructor() {
|
|
9
|
+
// We will store { [instrumentId]: { new_users: Set(), closed_users: Set() } }
|
|
10
|
+
this.assetActivity = new Map();
|
|
11
|
+
this.mappings = null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
_initAsset(instrumentId) {
|
|
15
|
+
if (!this.assetActivity.has(instrumentId)) {
|
|
16
|
+
this.assetActivity.set(instrumentId, {
|
|
17
|
+
new_users: new Set(),
|
|
18
|
+
closed_users: new Set()
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
_getInstrumentIds(portfolio) {
|
|
24
|
+
const positions = portfolio?.AggregatedPositions || portfolio?.PublicPositions;
|
|
25
|
+
if (!positions || !Array.isArray(positions)) {
|
|
26
|
+
return new Set();
|
|
27
|
+
}
|
|
28
|
+
return new Set(positions.map(p => p.InstrumentID).filter(Boolean));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
process(todayPortfolio, yesterdayPortfolio, userId) {
|
|
32
|
+
if (!todayPortfolio || !yesterdayPortfolio) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const yIds = this._getInstrumentIds(yesterdayPortfolio);
|
|
37
|
+
const tIds = this._getInstrumentIds(todayPortfolio);
|
|
38
|
+
|
|
39
|
+
// Find new positions (in today but not yesterday)
|
|
40
|
+
for (const tId of tIds) {
|
|
41
|
+
if (!yIds.has(tId)) {
|
|
42
|
+
this._initAsset(tId);
|
|
43
|
+
this.assetActivity.get(tId).new_users.add(userId);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Find closed positions (in yesterday but not today)
|
|
48
|
+
for (const yId of yIds) {
|
|
49
|
+
if (!tIds.has(yId)) {
|
|
50
|
+
this._initAsset(yId);
|
|
51
|
+
this.assetActivity.get(yId).closed_users.add(userId);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async getResult() {
|
|
57
|
+
if (!this.mappings) {
|
|
58
|
+
this.mappings = await loadInstrumentMappings();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const result = {};
|
|
62
|
+
for (const [instrumentId, data] of this.assetActivity.entries()) {
|
|
63
|
+
const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
|
|
64
|
+
|
|
65
|
+
const openCount = data.new_users.size;
|
|
66
|
+
const closeCount = data.closed_users.size;
|
|
67
|
+
|
|
68
|
+
if (openCount > 0 || closeCount > 0) {
|
|
69
|
+
result[ticker] = {
|
|
70
|
+
opened_by_user_count: openCount,
|
|
71
|
+
closed_by_user_count: closeCount,
|
|
72
|
+
// "Net User Flow" - positive means more users joined than left
|
|
73
|
+
net_user_flow: openCount - closeCount
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
reset() {
|
|
81
|
+
this.assetActivity.clear();
|
|
82
|
+
this.mappings = null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = DailyAssetActivity;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tracks "tinkering" activity from speculators.
|
|
3
|
+
* Instead of just opening/closing, this counts how many users
|
|
4
|
+
* actively *adjusted* the SL, TP, or TSL on existing trades.
|
|
5
|
+
*/
|
|
6
|
+
class SpeculatorAdjustmentActivity {
|
|
7
|
+
constructor() {
|
|
8
|
+
// Use Sets to count unique users
|
|
9
|
+
this.sl_adjusted_users = new Set();
|
|
10
|
+
this.tp_adjusted_users = new Set();
|
|
11
|
+
this.tsl_toggled_users = new Set();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
_getPublicPositionsMap(portfolio) {
|
|
15
|
+
const positions = portfolio?.PublicPositions;
|
|
16
|
+
if (!positions || !Array.isArray(positions)) {
|
|
17
|
+
return new Map();
|
|
18
|
+
}
|
|
19
|
+
// Map<PositionID, PositionObject>
|
|
20
|
+
return new Map(positions.map(p => [p.PositionID, p]));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
process(todayPortfolio, yesterdayPortfolio, userId) {
|
|
24
|
+
// This calculation is only for speculators
|
|
25
|
+
if (todayPortfolio?.context?.userType !== 'speculator' || !yesterdayPortfolio) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const yPosMap = this._getPublicPositionsMap(yesterdayPortfolio);
|
|
30
|
+
const tPosMap = this._getPublicPositionsMap(todayPortfolio);
|
|
31
|
+
|
|
32
|
+
if (yPosMap.size === 0 || tPosMap.size === 0) {
|
|
33
|
+
return; // No positions to compare
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const [tPosId, tPos] of tPosMap.entries()) {
|
|
37
|
+
// Check if this position existed yesterday
|
|
38
|
+
if (yPosMap.has(tPosId)) {
|
|
39
|
+
const yPos = yPosMap.get(tPosId);
|
|
40
|
+
|
|
41
|
+
// 1. Check for Stop Loss adjustment
|
|
42
|
+
if (tPos.StopLossRate !== yPos.StopLossRate) {
|
|
43
|
+
this.sl_adjusted_users.add(userId);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 2. Check for Take Profit adjustment
|
|
47
|
+
if (tPos.TakeProfitRate !== yPos.TakeProfitRate) {
|
|
48
|
+
this.tp_adjusted_users.add(userId);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 3. Check if TSL was toggled on or off
|
|
52
|
+
if (tPos.IsTslEnabled !== yPos.IsTslEnabled) {
|
|
53
|
+
this.tsl_toggled_users.add(userId);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getResult() {
|
|
60
|
+
return {
|
|
61
|
+
// Count of unique users who adjusted at least one trade's SL
|
|
62
|
+
unique_users_adjusted_sl: this.sl_adjusted_users.size,
|
|
63
|
+
// Count of unique users who adjusted at least one trade's TP
|
|
64
|
+
unique_users_adjusted_tp: this.tp_adjusted_users.size,
|
|
65
|
+
// Count of unique users who toggled TSL on or off
|
|
66
|
+
unique_users_toggled_tsl: this.tsl_toggled_users.size
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
reset() {
|
|
71
|
+
this.sl_adjusted_users.clear();
|
|
72
|
+
this.tp_adjusted_users.clear();
|
|
73
|
+
this.tsl_toggled_users.clear();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = SpeculatorAdjustmentActivity;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Meta-calculation (Pass 3) that tracks the performance
|
|
3
|
+
* of capital "vintages" by analyzing the market returns of assets
|
|
4
|
+
* that were bought following a crowd-wide deposit signal.
|
|
5
|
+
*
|
|
6
|
+
* This answers: "Does capital deployed from a fresh deposit event
|
|
7
|
+
* outperform capital deployed earlier?"
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
class CapitalVintagePerformance {
|
|
11
|
+
constructor() {
|
|
12
|
+
// How many days to look back/forward to measure performance
|
|
13
|
+
this.PERFORMANCE_WINDOW_DAYS = 7;
|
|
14
|
+
this.dependenciesLoaded = false;
|
|
15
|
+
this.priceMap = null;
|
|
16
|
+
this.tickerToIdMap = null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Helper to load all dependencies in parallel
|
|
21
|
+
*/
|
|
22
|
+
async _loadDependencies(calculationUtils) {
|
|
23
|
+
if (this.dependenciesLoaded) return;
|
|
24
|
+
|
|
25
|
+
const { loadAllPriceData, loadInstrumentMappings } = calculationUtils;
|
|
26
|
+
|
|
27
|
+
const [priceData, mappings] = await Promise.all([
|
|
28
|
+
loadAllPriceData(),
|
|
29
|
+
loadInstrumentMappings()
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
this.priceMap = priceData;
|
|
33
|
+
|
|
34
|
+
// Create a reverse map for easy lookup
|
|
35
|
+
this.tickerToIdMap = {};
|
|
36
|
+
if (mappings && mappings.instrumentToTicker) {
|
|
37
|
+
for (const [id, ticker] of Object.entries(mappings.instrumentToTicker)) {
|
|
38
|
+
this.tickerToIdMap[ticker] = id;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.dependenciesLoaded = true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Helper to get a date string X days from a base date
|
|
47
|
+
*/
|
|
48
|
+
_getDateStr(baseDateStr, daysOffset) {
|
|
49
|
+
const date = new Date(baseDateStr + 'T00:00:00Z');
|
|
50
|
+
date.setUTCDate(date.getUTCDate() + daysOffset);
|
|
51
|
+
return date.toISOString().slice(0, 10);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Helper to calculate the average return of a basket of assets
|
|
56
|
+
* over a specified period.
|
|
57
|
+
*/
|
|
58
|
+
_calculateBasketPerformance(assets, startDateStr, endDateStr) {
|
|
59
|
+
if (!assets || assets.length === 0) {
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let totalReturn = 0;
|
|
64
|
+
let validAssets = 0;
|
|
65
|
+
|
|
66
|
+
for (const asset of assets) {
|
|
67
|
+
const ticker = asset.ticker;
|
|
68
|
+
const instrumentId = this.tickerToIdMap[ticker];
|
|
69
|
+
|
|
70
|
+
if (!instrumentId || !this.priceMap[instrumentId]) {
|
|
71
|
+
continue; // No price data for this ticker
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const startPrice = this.priceMap[instrumentId][startDateStr];
|
|
75
|
+
const endPrice = this.priceMap[instrumentId][endDateStr];
|
|
76
|
+
|
|
77
|
+
if (startPrice && endPrice && startPrice > 0) {
|
|
78
|
+
const assetReturn = (endPrice - startPrice) / startPrice;
|
|
79
|
+
totalReturn += assetReturn;
|
|
80
|
+
validAssets++;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (validAssets === 0) return 0;
|
|
85
|
+
return (totalReturn / validAssets) * 100; // Return as percentage
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {string} dateStr The date to run the analysis for (e.g., "2025-10-31").
|
|
90
|
+
* @param {object} dependencies The shared dependencies (db, logger, calculationUtils).
|
|
91
|
+
* @param {object} config The computation system configuration.
|
|
92
|
+
* @returns {Promise<object|null>} The analysis result or null.
|
|
93
|
+
*/
|
|
94
|
+
async process(dateStr, dependencies, config) {
|
|
95
|
+
const { db, logger, calculationUtils } = dependencies;
|
|
96
|
+
|
|
97
|
+
// 1. Load all price/mapping data
|
|
98
|
+
await this._loadDependencies(calculationUtils);
|
|
99
|
+
|
|
100
|
+
// 2. Define and fetch dependency: cash-flow-deployment
|
|
101
|
+
const depRef = db.collection(config.resultsCollection).doc(dateStr)
|
|
102
|
+
.collection('results').doc('meta')
|
|
103
|
+
.collection('computations').doc('cash-flow-deployment');
|
|
104
|
+
|
|
105
|
+
const snapshot = await depRef.get();
|
|
106
|
+
|
|
107
|
+
if (!snapshot.exists || snapshot.data().status !== 'analysis_complete') {
|
|
108
|
+
logger.log('WARN', `[CapitalVintage] Skipping ${dateStr}, no valid 'cash-flow-deployment' data found.`);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const data = snapshot.data();
|
|
113
|
+
const topAssets = data.top_deployment_assets; // [{ ticker: 'AAPL', ... }]
|
|
114
|
+
const signalDate = data.signal_date; // The day the deposit signal occurred
|
|
115
|
+
const deploymentDate = data.analysis_date; // The day the capital was spent (dateStr)
|
|
116
|
+
|
|
117
|
+
if (!topAssets || topAssets.length === 0) {
|
|
118
|
+
logger.log('INFO', `[CapitalVintage] No top assets deployed on ${dateStr}.`);
|
|
119
|
+
return { status: 'no_deployment' };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 3. Define performance windows
|
|
123
|
+
// "Before" window: 7 days leading up to the signal
|
|
124
|
+
const preSignalStart = this._getDateStr(signalDate, -this.PERFORMANCE_WINDOW_DAYS);
|
|
125
|
+
const preSignalEnd = signalDate;
|
|
126
|
+
|
|
127
|
+
// "After" window: 7 days starting from the deployment
|
|
128
|
+
const postDeployStart = deploymentDate;
|
|
129
|
+
const postDeployEnd = this._getDateStr(deploymentDate, this.PERFORMANCE_WINDOW_DAYS);
|
|
130
|
+
|
|
131
|
+
// 4. Calculate performance
|
|
132
|
+
const preSignalReturnPct = this._calculateBasketPerformance(
|
|
133
|
+
topAssets, preSignalStart, preSignalEnd
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const postDeploymentReturnPct = this._calculateBasketPerformance(
|
|
137
|
+
topAssets, postDeployStart, postDeployEnd
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const momentum = postDeploymentReturnPct - preSignalReturnPct;
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
status: 'analysis_complete',
|
|
144
|
+
signal_date: signalDate,
|
|
145
|
+
deployment_date: deploymentDate,
|
|
146
|
+
performance_window_days: this.PERFORMANCE_WINDOW_DAYS,
|
|
147
|
+
deployed_assets: topAssets.map(a => a.ticker),
|
|
148
|
+
pre_signal_return_pct: preSignalReturnPct,
|
|
149
|
+
post_deployment_return_pct: postDeploymentReturnPct,
|
|
150
|
+
return_momentum: momentum,
|
|
151
|
+
interpretation: "Measures the 7-day return of deployed assets *after* deployment vs. 7-days *before* the signal."
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async getResult() { return null; }
|
|
156
|
+
reset() {}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = CapitalVintagePerformance;
|