aiden-shared-calculations-unified 1.0.23 → 1.0.24

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.
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiden-shared-calculations-unified",
3
- "version": "1.0.23",
3
+ "version": "1.0.24",
4
4
  "description": "Shared calculation modules for the BullTrackers Computation System.",
5
5
  "main": "index.js",
6
6
  "files": [