aiden-shared-calculations-unified 1.0.35 → 1.0.37
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/README.MD +77 -77
- package/calculations/activity/historical/activity_by_pnl_status.js +85 -85
- package/calculations/activity/historical/daily_asset_activity.js +85 -85
- package/calculations/activity/historical/daily_user_activity_tracker.js +144 -144
- package/calculations/activity/historical/speculator_adjustment_activity.js +76 -76
- package/calculations/asset_metrics/asset_position_size.js +57 -57
- package/calculations/backtests/strategy-performance.js +229 -245
- package/calculations/behavioural/historical/asset_crowd_flow.js +165 -165
- package/calculations/behavioural/historical/drawdown_response.js +58 -58
- package/calculations/behavioural/historical/dumb-cohort-flow.js +217 -249
- package/calculations/behavioural/historical/gain_response.js +57 -57
- package/calculations/behavioural/historical/in_loss_asset_crowd_flow.js +98 -98
- package/calculations/behavioural/historical/in_profit_asset_crowd_flow.js +99 -99
- package/calculations/behavioural/historical/paper_vs_diamond_hands.js +39 -39
- package/calculations/behavioural/historical/position_count_pnl.js +67 -67
- package/calculations/behavioural/historical/smart-cohort-flow.js +217 -250
- package/calculations/behavioural/historical/smart_money_flow.js +165 -165
- package/calculations/behavioural/historical/user-investment-profile.js +358 -412
- package/calculations/capital_flow/historical/crowd-cash-flow-proxy.js +121 -121
- package/calculations/capital_flow/historical/deposit_withdrawal_percentage.js +117 -117
- package/calculations/capital_flow/historical/new_allocation_percentage.js +49 -49
- package/calculations/insights/daily_bought_vs_sold_count.js +55 -55
- package/calculations/insights/daily_buy_sell_sentiment_count.js +49 -49
- package/calculations/insights/daily_ownership_delta.js +55 -55
- package/calculations/insights/daily_total_positions_held.js +39 -39
- package/calculations/meta/capital_deployment_strategy.js +129 -137
- package/calculations/meta/capital_liquidation_performance.js +121 -163
- package/calculations/meta/capital_vintage_performance.js +121 -158
- package/calculations/meta/cash-flow-deployment.js +110 -124
- package/calculations/meta/cash-flow-liquidation.js +126 -142
- package/calculations/meta/crowd_sharpe_ratio_proxy.js +83 -91
- package/calculations/meta/profit_cohort_divergence.js +77 -91
- package/calculations/meta/smart-dumb-divergence-index.js +116 -138
- package/calculations/meta/social_flow_correlation.js +99 -125
- package/calculations/pnl/asset_pnl_status.js +46 -46
- package/calculations/pnl/historical/profitability_migration.js +57 -57
- package/calculations/pnl/historical/user_profitability_tracker.js +117 -117
- package/calculations/pnl/profitable_and_unprofitable_status.js +64 -64
- package/calculations/sectors/historical/diversification_pnl.js +76 -76
- package/calculations/sectors/historical/sector_rotation.js +67 -67
- package/calculations/sentiment/historical/crowd_conviction_score.js +80 -80
- package/calculations/socialPosts/social-asset-posts-trend.js +52 -52
- package/calculations/socialPosts/social-top-mentioned-words.js +102 -102
- package/calculations/socialPosts/social-topic-interest-evolution.js +53 -53
- package/calculations/socialPosts/social-word-mentions-trend.js +62 -62
- package/calculations/socialPosts/social_activity_aggregation.js +103 -103
- package/calculations/socialPosts/social_event_correlation.js +121 -121
- package/calculations/socialPosts/social_sentiment_aggregation.js +114 -114
- package/calculations/speculators/historical/risk_appetite_change.js +54 -54
- package/calculations/speculators/historical/tsl_effectiveness.js +74 -74
- package/index.js +33 -33
- package/package.json +32 -32
- package/utils/firestore_utils.js +76 -76
- package/utils/price_data_provider.js +142 -142
- package/utils/sector_mapping_provider.js +74 -74
- package/calculations/capital_flow/historical/reallocation_increase_percentage.js +0 -63
- package/calculations/speculators/stop_loss_distance_by_sector_short_long_breakdown.js +0 -91
- package/calculations/speculators/stop_loss_distance_by_ticker_short_long_breakdown.js +0 -73
package/README.MD
CHANGED
|
@@ -1,78 +1,78 @@
|
|
|
1
|
-
# Unified Calculations Package (`aiden-shared-calculations-unified`)
|
|
2
|
-
|
|
3
|
-
**Version:** 1.0.0
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
This package centralizes all data calculation logic for the BullTrackers project. It provides a standardized structure for calculations run by the unified Computation System and includes shared utility functions. Calculations are dynamically loaded and categorized.
|
|
8
|
-
|
|
9
|
-
## Package Structure
|
|
10
|
-
|
|
11
|
-
### `/utils`
|
|
12
|
-
|
|
13
|
-
Shared utility functions used by calculations or the systems consuming them.
|
|
14
|
-
|
|
15
|
-
* `firestore_utils.js`: Provides a resilient `withRetry` wrapper for Firestore operations using exponential backoff.
|
|
16
|
-
* `sector_mapping_provider.js`: Provides functions (`loadInstrumentMappings`, `getInstrumentSectorMap`) to fetch and cache instrument-to-ticker and instrument-to-sector mappings from Firestore.
|
|
17
|
-
|
|
18
|
-
### `/calculations`
|
|
19
|
-
|
|
20
|
-
Contains the core calculation logic, organized into subdirectories representing categories.
|
|
21
|
-
|
|
22
|
-
* **Calculation Class Structure:** Each `.js` file defines a class responsible for a specific metric. Every class **must** implement:
|
|
23
|
-
* `constructor()`: Initializes any internal state needed for aggregation.
|
|
24
|
-
* `process(portfolioData, userId, context)` OR `process(todayPortfolio, yesterdayPortfolio, userId)`: Processes a single user's data. The signature depends on whether the calculation requires historical comparison. `context` provides shared data like mappings.
|
|
25
|
-
* `getResult()`: Returns the final, calculated result for the aggregation period. **Crucially, this method must perform any final averaging (e.g., sum/count) itself.** It should return the final value or object ready for storage, not raw components.
|
|
26
|
-
* `reset()`: (Optional but recommended) Resets the internal state, often used by the calling system between processing batches or days.
|
|
27
|
-
|
|
28
|
-
* **Categories (Examples based on your files):**
|
|
29
|
-
* `/asset_metrics`: Calculations focused on individual assets (e.g., `asset_dollar_metrics`, `asset_position_size`).
|
|
30
|
-
* `/behavioural`: Calculations analyzing user trading patterns (e.g., `drawdown_response`, `gain_response`, `paper_vs_diamond_hands`, `position_count_pnl`, `smart_money_flow`).
|
|
31
|
-
* `/pnl`: Profit and Loss related calculations (e.g., `asset_pnl_status`, `average_daily_pnl_all_users`, `average_daily_pnl_per_sector`, `average_daily_pnl_per_stock`, `average_daily_position_pnl`, `pnl_distribution_per_stock`, `profitability_migration`, `profitability_ratio_per_stock`, `profitability_skew_per_stock`, `user_profitability_tracker`).
|
|
32
|
-
* `/sanity`: Basic checks and counts (e.g., `users_processed`).
|
|
33
|
-
* `/sectors`: Calculations aggregated by market sector (e.g., `diversification_pnl`, `sector_dollar_metrics`, `sector_rotation`, `total_long_per_sector`, `total_short_per_sector`).
|
|
34
|
-
* `/sentiment`: Calculations related to market sentiment (e.g., `crowd_conviction_score`).
|
|
35
|
-
* `/short_and_long_stats`: Specific counts for short and long positions (e.g., `long_position_per_stock`, `sentiment_per_stock`, `short_position_per_stock`, `total_long_figures`, `total_short_figures`).
|
|
36
|
-
* `/speculators`: Calculations **specifically** for the 'speculator' user type, often involving leverage, stop-loss, or take-profit data (e.g., `distance_to_stop_loss_per_leverage`, `distance_to_tp_per_leverage`, `entry_distance_to_sl_per_leverage`, `entry_distance_to_tp_per_leverage`, `holding_duration_per_asset`, `leverage_per_asset`, `leverage_per_sector`, `risk_appetite_change`, `risk_reward_ratio_per_asset`, `speculator_asset_sentiment`, `speculator_danger_zone`, `stop_loss_distance_by_sector_short_long_breakdown`, `stop_loss_distance_by_ticker_short_long_breakdown`, `stop_loss_per_asset`, `take_profit_per_asset`, `tsl_effectiveness`, `tsl_per_asset`).
|
|
37
|
-
|
|
38
|
-
* **Output Formats:** Calculations should adhere to the standardized output formats defined in `docs/Notes/output_formats.md`.
|
|
39
|
-
|
|
40
|
-
## Usage
|
|
41
|
-
|
|
42
|
-
This package is intended to be consumed primarily by the unified Computation System.
|
|
43
|
-
|
|
44
|
-
```javascript
|
|
45
|
-
// Example usage within Computation System
|
|
46
|
-
const { calculations, utils } = require('aiden-shared-calculations-unified');
|
|
47
|
-
|
|
48
|
-
const CalculationClass = calculations.pnl.average_daily_pnl_per_stock;
|
|
49
|
-
const calculator = new CalculationClass();
|
|
50
|
-
|
|
51
|
-
// ... load data ...
|
|
52
|
-
|
|
53
|
-
// In a loop for each user:
|
|
54
|
-
calculator.process(portfolioData, userId, context);
|
|
55
|
-
|
|
56
|
-
// After processing all users:
|
|
57
|
-
const results = await calculator.getResult();
|
|
58
|
-
|
|
59
|
-
// ... store results ...
|
|
60
|
-
````
|
|
61
|
-
|
|
62
|
-
## Contributing
|
|
63
|
-
|
|
64
|
-
*(Outline the process for adding new calculations)*
|
|
65
|
-
|
|
66
|
-
1. **Determine Category:** Decide which subdirectory (`/calculations/<category>`) the new metric belongs to. Create a new category if necessary.
|
|
67
|
-
2. **Create File:** Create a new `.js` file using kebab-case (e.g., `my-new-metric.js`).
|
|
68
|
-
3. **Implement Class:** Define the calculation class, ensuring it has `constructor`, `process`, and `getResult` methods adhering to the standards. Remember `getResult` must return the *final* computed value.
|
|
69
|
-
4. **Add to Manifest:** The main `index.js` uses `require-all`, so the new calculation should be automatically included in the exports upon the next package build/publish, assuming the file naming and structure are correct.
|
|
70
|
-
5. **Publish:** Bump the package version (`npm version patch` or minor/major as appropriate) and publish (`npm publish --access public`).
|
|
71
|
-
6. **Update Consumer:** Update the version dependency in the Computation System's `package.json`.
|
|
72
|
-
|
|
73
|
-
<!-- end list -->
|
|
74
|
-
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
---
|
|
1
|
+
# Unified Calculations Package (`aiden-shared-calculations-unified`)
|
|
2
|
+
|
|
3
|
+
**Version:** 1.0.0
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This package centralizes all data calculation logic for the BullTrackers project. It provides a standardized structure for calculations run by the unified Computation System and includes shared utility functions. Calculations are dynamically loaded and categorized.
|
|
8
|
+
|
|
9
|
+
## Package Structure
|
|
10
|
+
|
|
11
|
+
### `/utils`
|
|
12
|
+
|
|
13
|
+
Shared utility functions used by calculations or the systems consuming them.
|
|
14
|
+
|
|
15
|
+
* `firestore_utils.js`: Provides a resilient `withRetry` wrapper for Firestore operations using exponential backoff.
|
|
16
|
+
* `sector_mapping_provider.js`: Provides functions (`loadInstrumentMappings`, `getInstrumentSectorMap`) to fetch and cache instrument-to-ticker and instrument-to-sector mappings from Firestore.
|
|
17
|
+
|
|
18
|
+
### `/calculations`
|
|
19
|
+
|
|
20
|
+
Contains the core calculation logic, organized into subdirectories representing categories.
|
|
21
|
+
|
|
22
|
+
* **Calculation Class Structure:** Each `.js` file defines a class responsible for a specific metric. Every class **must** implement:
|
|
23
|
+
* `constructor()`: Initializes any internal state needed for aggregation.
|
|
24
|
+
* `process(portfolioData, userId, context)` OR `process(todayPortfolio, yesterdayPortfolio, userId)`: Processes a single user's data. The signature depends on whether the calculation requires historical comparison. `context` provides shared data like mappings.
|
|
25
|
+
* `getResult()`: Returns the final, calculated result for the aggregation period. **Crucially, this method must perform any final averaging (e.g., sum/count) itself.** It should return the final value or object ready for storage, not raw components.
|
|
26
|
+
* `reset()`: (Optional but recommended) Resets the internal state, often used by the calling system between processing batches or days.
|
|
27
|
+
|
|
28
|
+
* **Categories (Examples based on your files):**
|
|
29
|
+
* `/asset_metrics`: Calculations focused on individual assets (e.g., `asset_dollar_metrics`, `asset_position_size`).
|
|
30
|
+
* `/behavioural`: Calculations analyzing user trading patterns (e.g., `drawdown_response`, `gain_response`, `paper_vs_diamond_hands`, `position_count_pnl`, `smart_money_flow`).
|
|
31
|
+
* `/pnl`: Profit and Loss related calculations (e.g., `asset_pnl_status`, `average_daily_pnl_all_users`, `average_daily_pnl_per_sector`, `average_daily_pnl_per_stock`, `average_daily_position_pnl`, `pnl_distribution_per_stock`, `profitability_migration`, `profitability_ratio_per_stock`, `profitability_skew_per_stock`, `user_profitability_tracker`).
|
|
32
|
+
* `/sanity`: Basic checks and counts (e.g., `users_processed`).
|
|
33
|
+
* `/sectors`: Calculations aggregated by market sector (e.g., `diversification_pnl`, `sector_dollar_metrics`, `sector_rotation`, `total_long_per_sector`, `total_short_per_sector`).
|
|
34
|
+
* `/sentiment`: Calculations related to market sentiment (e.g., `crowd_conviction_score`).
|
|
35
|
+
* `/short_and_long_stats`: Specific counts for short and long positions (e.g., `long_position_per_stock`, `sentiment_per_stock`, `short_position_per_stock`, `total_long_figures`, `total_short_figures`).
|
|
36
|
+
* `/speculators`: Calculations **specifically** for the 'speculator' user type, often involving leverage, stop-loss, or take-profit data (e.g., `distance_to_stop_loss_per_leverage`, `distance_to_tp_per_leverage`, `entry_distance_to_sl_per_leverage`, `entry_distance_to_tp_per_leverage`, `holding_duration_per_asset`, `leverage_per_asset`, `leverage_per_sector`, `risk_appetite_change`, `risk_reward_ratio_per_asset`, `speculator_asset_sentiment`, `speculator_danger_zone`, `stop_loss_distance_by_sector_short_long_breakdown`, `stop_loss_distance_by_ticker_short_long_breakdown`, `stop_loss_per_asset`, `take_profit_per_asset`, `tsl_effectiveness`, `tsl_per_asset`).
|
|
37
|
+
|
|
38
|
+
* **Output Formats:** Calculations should adhere to the standardized output formats defined in `docs/Notes/output_formats.md`.
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
This package is intended to be consumed primarily by the unified Computation System.
|
|
43
|
+
|
|
44
|
+
```javascript
|
|
45
|
+
// Example usage within Computation System
|
|
46
|
+
const { calculations, utils } = require('aiden-shared-calculations-unified');
|
|
47
|
+
|
|
48
|
+
const CalculationClass = calculations.pnl.average_daily_pnl_per_stock;
|
|
49
|
+
const calculator = new CalculationClass();
|
|
50
|
+
|
|
51
|
+
// ... load data ...
|
|
52
|
+
|
|
53
|
+
// In a loop for each user:
|
|
54
|
+
calculator.process(portfolioData, userId, context);
|
|
55
|
+
|
|
56
|
+
// After processing all users:
|
|
57
|
+
const results = await calculator.getResult();
|
|
58
|
+
|
|
59
|
+
// ... store results ...
|
|
60
|
+
````
|
|
61
|
+
|
|
62
|
+
## Contributing
|
|
63
|
+
|
|
64
|
+
*(Outline the process for adding new calculations)*
|
|
65
|
+
|
|
66
|
+
1. **Determine Category:** Decide which subdirectory (`/calculations/<category>`) the new metric belongs to. Create a new category if necessary.
|
|
67
|
+
2. **Create File:** Create a new `.js` file using kebab-case (e.g., `my-new-metric.js`).
|
|
68
|
+
3. **Implement Class:** Define the calculation class, ensuring it has `constructor`, `process`, and `getResult` methods adhering to the standards. Remember `getResult` must return the *final* computed value.
|
|
69
|
+
4. **Add to Manifest:** The main `index.js` uses `require-all`, so the new calculation should be automatically included in the exports upon the next package build/publish, assuming the file naming and structure are correct.
|
|
70
|
+
5. **Publish:** Bump the package version (`npm version patch` or minor/major as appropriate) and publish (`npm publish --access public`).
|
|
71
|
+
6. **Update Consumer:** Update the version dependency in the Computation System's `package.json`.
|
|
72
|
+
|
|
73
|
+
<!-- end list -->
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
78
|
```
|
|
@@ -1,86 +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
|
-
|
|
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
86
|
module.exports = ActivityByPnlStatus;
|
|
@@ -1,86 +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
|
-
|
|
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
86
|
module.exports = DailyAssetActivity;
|