aiden-shared-calculations-unified 1.0.64 → 1.0.65
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 +1 -1
- package/calculations/activity/historical/activity_by_pnl_status.js +33 -0
- package/calculations/activity/historical/daily_asset_activity.js +42 -0
- package/calculations/activity/historical/daily_user_activity_tracker.js +37 -0
- package/calculations/activity/historical/speculator_adjustment_activity.js +26 -0
- package/calculations/asset_metrics/asset_position_size.js +36 -0
- package/calculations/backtests/strategy-performance.js +41 -0
- package/calculations/behavioural/historical/asset_crowd_flow.js +124 -127
- package/calculations/behavioural/historical/drawdown_response.js +113 -35
- package/calculations/behavioural/historical/dumb-cohort-flow.js +191 -171
- package/calculations/behavioural/historical/gain_response.js +113 -34
- package/calculations/behavioural/historical/historical_performance_aggregator.js +63 -48
- package/calculations/behavioural/historical/in_loss_asset_crowd_flow.js +159 -63
- package/calculations/behavioural/historical/in_profit_asset_crowd_flow.js +159 -64
- package/calculations/behavioural/historical/paper_vs_diamond_hands.js +86 -19
- package/calculations/behavioural/historical/position_count_pnl.js +91 -39
- package/calculations/behavioural/historical/smart-cohort-flow.js +192 -172
- package/calculations/behavioural/historical/smart_money_flow.js +160 -151
- package/calculations/capital_flow/historical/crowd-cash-flow-proxy.js +95 -89
- package/calculations/capital_flow/historical/deposit_withdrawal_percentage.js +88 -81
- package/calculations/capital_flow/historical/new_allocation_percentage.js +75 -26
- package/calculations/capital_flow/historical/reallocation_increase_percentage.js +73 -32
- package/calculations/insights/daily_buy_sell_sentiment_count.js +47 -32
- package/calculations/insights/daily_total_positions_held.js +28 -24
- package/calculations/insights/historical/daily_bought_vs_sold_count.js +101 -36
- package/calculations/insights/historical/daily_ownership_delta.js +95 -32
- package/calculations/meta/capital_deployment_strategy.js +78 -110
- package/calculations/meta/capital_liquidation_performance.js +114 -111
- package/calculations/meta/cash-flow-deployment.js +114 -107
- package/calculations/meta/cash-flow-liquidation.js +114 -107
- package/calculations/meta/crowd_sharpe_ratio_proxy.js +94 -54
- package/calculations/meta/negative_expectancy_cohort_flow.js +185 -177
- package/calculations/meta/positive_expectancy_cohort_flow.js +186 -181
- package/calculations/meta/profit_cohort_divergence.js +83 -59
- package/calculations/meta/shark_attack_signal.js +91 -39
- package/calculations/meta/smart-dumb-divergence-index.js +114 -98
- package/calculations/meta/smart_dumb_divergence_index_v2.js +109 -98
- package/calculations/meta/social-predictive-regime-state.js +76 -155
- package/calculations/meta/social-topic-driver-index.js +74 -127
- package/calculations/meta/user_expectancy_score.js +83 -31
- package/calculations/pnl/asset_pnl_status.js +120 -31
- package/calculations/pnl/average_daily_pnl_all_users.js +42 -27
- package/calculations/pnl/average_daily_pnl_per_sector.js +84 -26
- package/calculations/pnl/average_daily_pnl_per_stock.js +71 -29
- package/calculations/pnl/average_daily_position_pnl.js +49 -21
- package/calculations/pnl/historical/profitability_migration.js +81 -35
- package/calculations/pnl/historical/user_profitability_tracker.js +107 -104
- package/calculations/pnl/pnl_distribution_per_stock.js +65 -45
- package/calculations/pnl/profitability_ratio_per_stock.js +78 -21
- package/calculations/pnl/profitability_skew_per_stock.js +86 -31
- package/calculations/pnl/profitable_and_unprofitable_status.js +45 -45
- package/calculations/sanity/users_processed.js +24 -1
- package/calculations/sectors/historical/diversification_pnl.js +104 -42
- package/calculations/sectors/historical/sector_rotation.js +94 -45
- package/calculations/sectors/total_long_per_sector.js +55 -20
- package/calculations/sectors/total_short_per_sector.js +55 -20
- package/calculations/sentiment/historical/crowd_conviction_score.js +233 -53
- package/calculations/short_and_long_stats/long_position_per_stock.js +50 -14
- package/calculations/short_and_long_stats/sentiment_per_stock.js +76 -19
- package/calculations/short_and_long_stats/short_position_per_stock.js +50 -13
- package/calculations/short_and_long_stats/total_long_figures.js +34 -13
- package/calculations/short_and_long_stats/total_short_figures.js +34 -14
- package/calculations/socialPosts/social-asset-posts-trend.js +96 -29
- package/calculations/socialPosts/social-top-mentioned-words.js +95 -74
- package/calculations/socialPosts/social-topic-interest-evolution.js +92 -29
- package/calculations/socialPosts/social-topic-sentiment-matrix.js +70 -78
- package/calculations/socialPosts/social-word-mentions-trend.js +96 -38
- package/calculations/socialPosts/social_activity_aggregation.js +106 -77
- package/calculations/socialPosts/social_sentiment_aggregation.js +115 -86
- package/calculations/speculators/distance_to_stop_loss_per_leverage.js +82 -43
- package/calculations/speculators/distance_to_tp_per_leverage.js +81 -42
- package/calculations/speculators/entry_distance_to_sl_per_leverage.js +80 -44
- package/calculations/speculators/entry_distance_to_tp_per_leverage.js +81 -44
- package/calculations/speculators/historical/risk_appetite_change.js +89 -32
- package/calculations/speculators/historical/tsl_effectiveness.js +57 -47
- package/calculations/speculators/holding_duration_per_asset.js +83 -23
- package/calculations/speculators/leverage_per_asset.js +68 -19
- package/calculations/speculators/leverage_per_sector.js +86 -25
- package/calculations/speculators/risk_reward_ratio_per_asset.js +82 -28
- package/calculations/speculators/speculator_asset_sentiment.js +100 -48
- package/calculations/speculators/speculator_danger_zone.js +101 -33
- package/calculations/speculators/stop_loss_distance_by_sector_short_long_breakdown.js +93 -66
- package/calculations/speculators/stop_loss_distance_by_ticker_short_long_breakdown.js +94 -47
- package/calculations/speculators/stop_loss_per_asset.js +94 -26
- package/calculations/speculators/take_profit_per_asset.js +95 -27
- package/calculations/speculators/tsl_per_asset.js +77 -23
- package/package.json +1 -1
- package/utils/price_data_provider.js +142 -142
- package/utils/sector_mapping_provider.js +74 -74
package/README.MD
CHANGED
|
@@ -21,7 +21,7 @@ Contains the core calculation logic, organized into subdirectories representing
|
|
|
21
21
|
|
|
22
22
|
* **Calculation Class Structure:** Each `.js` file defines a class responsible for a specific metric. Every class **must** implement:
|
|
23
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.
|
|
24
|
+
* `process(portfolioData, userId, context)` OR `process(todayPortfolio, yesterdayPortfolio, userId, context)`: Processes a single user's data. The signature depends on whether the calculation requires historical comparison. `context` provides shared data like mappings.
|
|
25
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
26
|
* `reset()`: (Optional but recommended) Resets the internal state, often used by the calling system between processing batches or days.
|
|
27
27
|
|
|
@@ -15,6 +15,39 @@ class ActivityByPnlStatus {
|
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Defines the output schema for this calculation.
|
|
20
|
+
* @returns {object} JSON Schema object
|
|
21
|
+
*/
|
|
22
|
+
static getSchema() {
|
|
23
|
+
return {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"description": "Measures profit-taking vs. capitulation by analyzing the P&L of closed positions.",
|
|
26
|
+
"properties": {
|
|
27
|
+
"profit_taking_rate_pct": {
|
|
28
|
+
"type": "number",
|
|
29
|
+
"description": "The percentage of profitable positions (from yesterday) that were closed today."
|
|
30
|
+
},
|
|
31
|
+
"capitulation_rate_pct": {
|
|
32
|
+
"type": "number",
|
|
33
|
+
"description": "The percentage of losing positions (from yesterday) that were closed today."
|
|
34
|
+
},
|
|
35
|
+
"raw_counts": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"description": "Raw counts used to calculate the rates.",
|
|
38
|
+
"properties": {
|
|
39
|
+
"profit_positions_closed": { "type": "number" },
|
|
40
|
+
"loss_positions_closed": { "type": "number" },
|
|
41
|
+
"total_profit_positions_available": { "type": "number" },
|
|
42
|
+
"total_loss_positions_available": { "type": "number" }
|
|
43
|
+
},
|
|
44
|
+
"required": ["profit_positions_closed", "loss_positions_closed", "total_profit_positions_available", "total_loss_positions_available"]
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"required": ["profit_taking_rate_pct", "capitulation_rate_pct", "raw_counts"]
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
18
51
|
_getPortfolioMap(portfolio) {
|
|
19
52
|
// We MUST use PositionID here to track specific trades, not just the asset
|
|
20
53
|
const positions = portfolio?.AggregatedPositions || portfolio?.PublicPositions;
|
|
@@ -11,6 +11,48 @@ class DailyAssetActivity {
|
|
|
11
11
|
this.mappings = null;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Defines the output schema for this calculation.
|
|
16
|
+
* @returns {object} JSON Schema object
|
|
17
|
+
*/
|
|
18
|
+
static getSchema() {
|
|
19
|
+
return {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"description": "Tracks the net flow of unique users opening or closing positions per asset.",
|
|
22
|
+
"patternProperties": {
|
|
23
|
+
// This matches any string key (which will be a ticker)
|
|
24
|
+
"^.*$": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"description": "User flow metrics for a specific asset ticker.",
|
|
27
|
+
"properties": {
|
|
28
|
+
"opened_by_user_count": {
|
|
29
|
+
"type": "number",
|
|
30
|
+
"description": "Count of unique users who opened a position in this asset."
|
|
31
|
+
},
|
|
32
|
+
"closed_by_user_count": {
|
|
33
|
+
"type": "number",
|
|
34
|
+
"description": "Count of unique users who closed their position in this asset."
|
|
35
|
+
},
|
|
36
|
+
"net_user_flow": {
|
|
37
|
+
"type": "number",
|
|
38
|
+
"description": "Net change in users (opened - closed)."
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"required": ["opened_by_user_count", "closed_by_user_count", "net_user_flow"]
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
// Use 'additionalProperties' as a fallback for patternProperties
|
|
45
|
+
"additionalProperties": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"properties": {
|
|
48
|
+
"opened_by_user_count": { "type": "number" },
|
|
49
|
+
"closed_by_user_count": { "type": "number" },
|
|
50
|
+
"net_user_flow": { "type": "number" }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
14
56
|
_initAsset(instrumentId) {
|
|
15
57
|
if (!this.assetActivity.has(instrumentId)) {
|
|
16
58
|
this.assetActivity.set(instrumentId, {
|
|
@@ -17,6 +17,43 @@ class DailyUserActivityTracker {
|
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Defines the output schema for this calculation.
|
|
22
|
+
* @returns {object} JSON Schema object
|
|
23
|
+
*/
|
|
24
|
+
static getSchema() {
|
|
25
|
+
return {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"description": "Tracks the count of daily active users based on portfolio changes.",
|
|
28
|
+
"properties": {
|
|
29
|
+
"rawActiveUserCount": {
|
|
30
|
+
"type": "number",
|
|
31
|
+
"description": "The total count of unique users who were active today (opened, closed, or reallocated)."
|
|
32
|
+
},
|
|
33
|
+
"activityBreakdown": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"description": "A breakdown of the first activity event type recorded for active users.",
|
|
36
|
+
"properties": {
|
|
37
|
+
"new_position": {
|
|
38
|
+
"type": "number",
|
|
39
|
+
"description": "Count of users whose first detected activity was opening a new position."
|
|
40
|
+
},
|
|
41
|
+
"closed_position": {
|
|
42
|
+
"type": "number",
|
|
43
|
+
"description": "Count of users whose first detected activity was closing an existing position."
|
|
44
|
+
},
|
|
45
|
+
"reallocation": {
|
|
46
|
+
"type": "number",
|
|
47
|
+
"description": "Count of users whose first detected activity was reallocating an existing position."
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"required": ["new_position", "closed_position", "reallocation"]
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"required": ["rawActiveUserCount", "activityBreakdown"]
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
20
57
|
/**
|
|
21
58
|
* Helper to get a simplified map of positions for comparison.
|
|
22
59
|
* @param {object} portfolio - A user's full portfolio object.
|
|
@@ -11,6 +11,32 @@ class SpeculatorAdjustmentActivity {
|
|
|
11
11
|
this.tsl_toggled_users = new Set();
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Defines the output schema for this calculation.
|
|
16
|
+
* @returns {object} JSON Schema object
|
|
17
|
+
*/
|
|
18
|
+
static getSchema() {
|
|
19
|
+
return {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"description": "Tracks unique speculators who 'tinkered' with open trades by adjusting SL, TP, or TSL.",
|
|
22
|
+
"properties": {
|
|
23
|
+
"unique_users_adjusted_sl": {
|
|
24
|
+
"type": "number",
|
|
25
|
+
"description": "Count of unique speculators who adjusted a Stop Loss on at least one position."
|
|
26
|
+
},
|
|
27
|
+
"unique_users_adjusted_tp": {
|
|
28
|
+
"type": "number",
|
|
29
|
+
"description": "Count of unique speculators who adjusted a Take Profit on at least one position."
|
|
30
|
+
},
|
|
31
|
+
"unique_users_toggled_tsl": {
|
|
32
|
+
"type": "number",
|
|
33
|
+
"description": "Count of unique speculators who enabled or disabled a Trailing Stop Loss on at least one position."
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"required": ["unique_users_adjusted_sl", "unique_users_adjusted_tp", "unique_users_toggled_tsl"]
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
14
40
|
_getPublicPositionsMap(portfolio) {
|
|
15
41
|
const positions = portfolio?.PublicPositions;
|
|
16
42
|
if (!positions || !Array.isArray(positions)) {
|
|
@@ -9,6 +9,42 @@ class AssetPositionSize {
|
|
|
9
9
|
this.mappings = null;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Defines the output schema for this calculation.
|
|
14
|
+
* @returns {object} JSON Schema object
|
|
15
|
+
*/
|
|
16
|
+
static getSchema() {
|
|
17
|
+
return {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"description": "Calculates the average position size (as a portfolio percentage) and holder count for each asset.",
|
|
20
|
+
"patternProperties": {
|
|
21
|
+
// This matches any string key (which will be a ticker)
|
|
22
|
+
"^.*$": {
|
|
23
|
+
"type": "object",
|
|
24
|
+
"description": "Metrics for a specific asset ticker.",
|
|
25
|
+
"properties": {
|
|
26
|
+
"average_position_size": {
|
|
27
|
+
"type": "number",
|
|
28
|
+
"description": "The average portfolio percentage allocated to this asset by users who hold it."
|
|
29
|
+
},
|
|
30
|
+
"position_count": {
|
|
31
|
+
"type": "number",
|
|
32
|
+
"description": "The total number of positions held in this asset across the user sample."
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"required": ["average_position_size", "position_count"]
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"additionalProperties": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"properties": {
|
|
41
|
+
"average_position_size": { "type": "number" },
|
|
42
|
+
"position_count": { "type": "number" }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
12
48
|
process(portfolioData, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights) {
|
|
13
49
|
const positions = portfolioData.AggregatedPositions || portfolioData.PublicPositions;
|
|
14
50
|
if (!positions || !Array.isArray(positions)) return;
|
|
@@ -23,6 +23,47 @@ class StrategyPerformance {
|
|
|
23
23
|
return ['smart-dumb-divergence-index', 'profit-cohort-divergence'];
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Defines the output schema for this calculation.
|
|
28
|
+
* @returns {object} JSON Schema object
|
|
29
|
+
*/
|
|
30
|
+
static getSchema() {
|
|
31
|
+
return {
|
|
32
|
+
"type": ["object", "null"],
|
|
33
|
+
"description": "Result of a full historical backtest of a trading strategy. Returns null if simulation fails.",
|
|
34
|
+
"properties": {
|
|
35
|
+
"strategyName": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"description": "The name of the simulated strategy."
|
|
38
|
+
},
|
|
39
|
+
"inceptionDate": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"description": "The first date a signal was found, marking the start of the backtest."
|
|
42
|
+
},
|
|
43
|
+
"endDate": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"description": "The final date of the backtest simulation."
|
|
46
|
+
},
|
|
47
|
+
"finalPortfolioValue": {
|
|
48
|
+
"type": "number",
|
|
49
|
+
"description": "The final equity value of the portfolio at the end of the simulation."
|
|
50
|
+
},
|
|
51
|
+
"totalReturnPercent": {
|
|
52
|
+
"type": "number",
|
|
53
|
+
"description": "The total percentage return of the strategy from inception to end date."
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
// "required" is not needed at the top level since the whole object can be null.
|
|
57
|
+
// If the object is *not* null, we can imply these fields are required.
|
|
58
|
+
"if": {
|
|
59
|
+
"type": "object"
|
|
60
|
+
},
|
|
61
|
+
"then": {
|
|
62
|
+
"required": ["strategyName", "inceptionDate", "endDate", "finalPortfolioValue", "totalReturnPercent"]
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
26
67
|
constructor() {
|
|
27
68
|
this.INITIAL_CASH = 100000;
|
|
28
69
|
this.TRADE_SIZE_USD = 5000;
|
|
@@ -1,165 +1,162 @@
|
|
|
1
|
-
const { loadAllPriceData, getDailyPriceChange } = require('../../../utils/price_data_provider');
|
|
2
|
-
const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
|
|
3
|
-
|
|
4
1
|
/**
|
|
5
|
-
* @fileoverview
|
|
6
|
-
*
|
|
7
|
-
* This isolates the change in an asset's average portfolio percentage
|
|
8
|
-
* that is *not* explained by the asset's own price movement.
|
|
2
|
+
* @fileoverview Calculation (Pass 2) for asset crowd flow.
|
|
9
3
|
*
|
|
10
|
-
*
|
|
4
|
+
* This metric calculates the "Net Crowd Flow Percentage" for each asset.
|
|
5
|
+
* It's defined as the net percentage of the crowd's capital flowing in or out
|
|
6
|
+
* of an asset, *adjusted for the asset's own price movement*.
|
|
11
7
|
*
|
|
12
|
-
*
|
|
13
|
-
* A negative value means the crowd actively sold (flowed out of) the asset.
|
|
8
|
+
* This isolates true buying/selling pressure from simple value changes.
|
|
14
9
|
*/
|
|
10
|
+
const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
|
|
11
|
+
|
|
15
12
|
class AssetCrowdFlow {
|
|
16
13
|
constructor() {
|
|
17
|
-
this.
|
|
18
|
-
this.user_count = 0;
|
|
19
|
-
this.priceMap = null;
|
|
14
|
+
this.assetData = new Map();
|
|
20
15
|
this.mappings = null;
|
|
21
|
-
this.dates = {}; // To store { today: '...', yesterday: '...' }
|
|
22
16
|
}
|
|
23
17
|
|
|
24
18
|
/**
|
|
25
|
-
*
|
|
19
|
+
* Defines the output schema for this calculation.
|
|
20
|
+
* @returns {object} JSON Schema object
|
|
26
21
|
*/
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
22
|
+
static getSchema() {
|
|
23
|
+
return {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"description": "Calculates the net capital flow % per asset, adjusted for price movements.",
|
|
26
|
+
"patternProperties": {
|
|
27
|
+
// Ticker
|
|
28
|
+
"^.*$": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"description": "Net crowd flow metrics for a specific asset ticker.",
|
|
31
|
+
"properties": {
|
|
32
|
+
"net_flow_percentage": {
|
|
33
|
+
"type": "number",
|
|
34
|
+
"description": "Net capital flow (buying/selling) as a percentage of total holdings, adjusted for price changes."
|
|
35
|
+
},
|
|
36
|
+
"total_invested_today": {
|
|
37
|
+
"type": "number",
|
|
38
|
+
"description": "Total USD value invested in this asset today by the sample."
|
|
39
|
+
},
|
|
40
|
+
"total_invested_yesterday": {
|
|
41
|
+
"type": "number",
|
|
42
|
+
"description": "Total USD value invested in this asset yesterday by the sample."
|
|
43
|
+
},
|
|
44
|
+
"price_change_contribution": {
|
|
45
|
+
"type": "number",
|
|
46
|
+
"description": "The portion of the value change attributable to the asset's price movement."
|
|
47
|
+
},
|
|
48
|
+
"net_flow_contribution": {
|
|
49
|
+
"type": "number",
|
|
50
|
+
"description": "The portion of the value change attributable to net buying or selling."
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"required": ["net_flow_percentage", "total_invested_today", "total_invested_yesterday"]
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"additionalProperties": {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"properties": {
|
|
59
|
+
"net_flow_percentage": { "type": "number" },
|
|
60
|
+
"total_invested_today": { "type": "number" },
|
|
61
|
+
"total_invested_yesterday": { "type": "number" },
|
|
62
|
+
"price_change_contribution": { "type": "number" },
|
|
63
|
+
"net_flow_contribution": { "type": "number" }
|
|
64
|
+
}
|
|
47
65
|
}
|
|
48
|
-
}
|
|
49
|
-
return valueMap;
|
|
66
|
+
};
|
|
50
67
|
}
|
|
51
68
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Capture dates from context on the first run
|
|
59
|
-
if (!this.dates.today && context.todayDateStr && context.yesterdayDateStr) {
|
|
60
|
-
this.dates.today = context.todayDateStr;
|
|
61
|
-
this.dates.yesterday = context.yesterdayDateStr;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const yesterdayValues = this._sumAssetValue(yesterdayPortfolio.AggregatedPositions);
|
|
65
|
-
const todayValues = this._sumAssetValue(todayPortfolio.AggregatedPositions);
|
|
66
|
-
|
|
67
|
-
// Use a set of all unique instruments held across both days
|
|
68
|
-
const allInstrumentIds = new Set([
|
|
69
|
-
...Object.keys(yesterdayValues),
|
|
70
|
-
...Object.keys(todayValues)
|
|
71
|
-
]);
|
|
69
|
+
_getPortfolioPositions(portfolio) {
|
|
70
|
+
return portfolio?.PublicPositions || portfolio?.AggregatedPositions;
|
|
71
|
+
}
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
this.
|
|
76
|
-
|
|
73
|
+
_initAsset(instrumentId) {
|
|
74
|
+
if (!this.assetData.has(instrumentId)) {
|
|
75
|
+
this.assetData.set(instrumentId, {
|
|
76
|
+
total_invested_yesterday: 0,
|
|
77
|
+
total_invested_today: 0,
|
|
78
|
+
price_change_yesterday: 0, // Used to calculate weighted avg price change
|
|
79
|
+
});
|
|
77
80
|
}
|
|
78
|
-
|
|
79
|
-
this.user_count++;
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
return null; // <--- Returns null if no users
|
|
83
|
+
process(todayPortfolio, yesterdayPortfolio) {
|
|
84
|
+
if (!todayPortfolio || !yesterdayPortfolio) {
|
|
85
|
+
return;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
const [priceData, mappingData] = await Promise.all([
|
|
92
|
-
loadAllPriceData(),
|
|
93
|
-
loadInstrumentMappings()
|
|
94
|
-
]);
|
|
95
|
-
this.priceMap = priceData;
|
|
96
|
-
this.mappings = mappingData;
|
|
97
|
-
|
|
98
|
-
if (!this.priceMap || Object.keys(this.priceMap).length === 0) {
|
|
99
|
-
console.error('[AssetCrowdFlow] CRITICAL: Price map is empty or failed to load. Aborting calculation to allow backfill.');
|
|
100
|
-
return null; // Return null to trigger backfill
|
|
101
|
-
}
|
|
88
|
+
const yPos = this._getPortfolioPositions(yesterdayPortfolio);
|
|
89
|
+
const tPos = this._getPortfolioPositions(todayPortfolio);
|
|
102
90
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return null; // <--- Return null on error
|
|
106
|
-
}
|
|
107
|
-
}
|
|
91
|
+
const yPosMap = new Map(yPos?.map(p => [p.InstrumentID, p]) || []);
|
|
92
|
+
const tPosMap = new Map(tPos?.map(p => [p.InstrumentID, p]) || []);
|
|
108
93
|
|
|
109
|
-
const
|
|
110
|
-
const todayStr = this.dates.today;
|
|
111
|
-
const yesterdayStr = this.dates.yesterday;
|
|
94
|
+
const allInstrumentIds = new Set([...yPosMap.keys(), ...tPosMap.keys()]);
|
|
112
95
|
|
|
113
|
-
for (const
|
|
114
|
-
|
|
115
|
-
|
|
96
|
+
for (const instrumentId of allInstrumentIds) {
|
|
97
|
+
if (!instrumentId) continue;
|
|
98
|
+
|
|
99
|
+
this._initAsset(instrumentId);
|
|
100
|
+
const asset = this.assetData.get(instrumentId);
|
|
116
101
|
|
|
117
|
-
const
|
|
118
|
-
const
|
|
102
|
+
const yP = yPosMap.get(instrumentId);
|
|
103
|
+
const tP = tPosMap.get(instrumentId);
|
|
119
104
|
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
// --- END FIX ---
|
|
105
|
+
const yInvested = yP?.InvestedAmount || yP?.Amount || 0;
|
|
106
|
+
const tInvested = tP?.InvestedAmount || tP?.Amount || 0;
|
|
123
107
|
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
|
|
108
|
+
if (yInvested > 0) {
|
|
109
|
+
asset.total_invested_yesterday += yInvested;
|
|
110
|
+
// Get price change from *yesterday's* portfolio
|
|
111
|
+
const yPriceChange = (yP?.PipsRate || 0) / (yP?.OpenRate || 1); // PipsRate holds 1-day change
|
|
112
|
+
asset.price_change_yesterday += yPriceChange * yInvested; // Weighted sum
|
|
113
|
+
}
|
|
114
|
+
if (tInvested > 0) {
|
|
115
|
+
asset.total_invested_today += tInvested;
|
|
128
116
|
}
|
|
129
|
-
|
|
130
|
-
// --- THIS IS THE LOGIC THAT IS BROKEN IN YOUR FILE ---
|
|
131
|
-
// Calculate expected day2 value from price movement
|
|
132
|
-
const expected_day2_value = avg_day1_value * (1 + priceChangePct);
|
|
133
|
-
|
|
134
|
-
// Net crowd flow = actual minus expected
|
|
135
|
-
const net_crowd_flow_pct = avg_day2_value - expected_day2_value;
|
|
136
|
-
|
|
137
|
-
finalResults[ticker] = {
|
|
138
|
-
net_crowd_flow_pct,
|
|
139
|
-
avg_value_day1_pct: avg_day1_value, // <-- Note: I've also fixed the key name here
|
|
140
|
-
avg_value_day2_pct: avg_day2_value, // <-- Note: I've also fixed the key name here
|
|
141
|
-
expected_day2_value,
|
|
142
|
-
price_change_pct: priceChangePct,
|
|
143
|
-
user_sample_size: this.user_count
|
|
144
|
-
};
|
|
145
|
-
// --- END BROKEN LOGIC ---
|
|
146
117
|
}
|
|
118
|
+
}
|
|
147
119
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
120
|
+
async getResult() {
|
|
121
|
+
if (!this.mappings) {
|
|
122
|
+
this.mappings = await loadInstrumentMappings();
|
|
151
123
|
}
|
|
152
124
|
|
|
153
|
-
|
|
125
|
+
const result = {};
|
|
126
|
+
|
|
127
|
+
for (const [instrumentId, data] of this.assetData.entries()) {
|
|
128
|
+
const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
|
|
129
|
+
|
|
130
|
+
const { total_invested_yesterday, total_invested_today, price_change_yesterday } = data;
|
|
131
|
+
|
|
132
|
+
if (total_invested_yesterday > 0) {
|
|
133
|
+
// Calculate the weighted average price change %
|
|
134
|
+
const avg_price_change_pct = price_change_yesterday / total_invested_yesterday;
|
|
135
|
+
|
|
136
|
+
// Estimate the value change *due to price*
|
|
137
|
+
const price_contribution = total_invested_yesterday * avg_price_change_pct;
|
|
138
|
+
|
|
139
|
+
// Estimate the value change *due to net flow* (buy/sell)
|
|
140
|
+
const flow_contribution = total_invested_today - (total_invested_yesterday + price_contribution);
|
|
141
|
+
|
|
142
|
+
// Calculate Net Flow as a percentage of yesterday's holdings
|
|
143
|
+
const net_flow_percentage = (flow_contribution / total_invested_yesterday) * 100;
|
|
144
|
+
|
|
145
|
+
result[ticker] = {
|
|
146
|
+
net_flow_percentage: net_flow_percentage,
|
|
147
|
+
total_invested_today: total_invested_today,
|
|
148
|
+
total_invested_yesterday: total_invested_yesterday,
|
|
149
|
+
price_change_contribution: price_contribution,
|
|
150
|
+
net_flow_contribution: flow_contribution
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return result;
|
|
154
155
|
}
|
|
155
156
|
|
|
156
|
-
|
|
157
157
|
reset() {
|
|
158
|
-
this.
|
|
159
|
-
this.user_count = 0;
|
|
160
|
-
this.priceMap = null;
|
|
158
|
+
this.assetData.clear();
|
|
161
159
|
this.mappings = null;
|
|
162
|
-
this.dates = {};
|
|
163
160
|
}
|
|
164
161
|
}
|
|
165
162
|
|