aiden-shared-calculations-unified 1.0.34 → 1.0.36
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 -170
- package/calculations/behavioural/historical/drawdown_response.js +58 -58
- package/calculations/behavioural/historical/dumb-cohort-flow.js +249 -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 +250 -250
- package/calculations/behavioural/historical/smart_money_flow.js +165 -165
- package/calculations/behavioural/historical/user-investment-profile.js +412 -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
|
@@ -1,99 +1,99 @@
|
|
|
1
|
-
const { loadAllPriceData, getDailyPriceChange } = require('../../../utils/price_data_provider');
|
|
2
|
-
const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* @fileoverview Calculates "Net Crowd Flow" for each asset, BUT
|
|
6
|
-
* *only* for the cohort of users who are currently IN LOSS
|
|
7
|
-
* on their positions for that asset.
|
|
8
|
-
*/
|
|
9
|
-
class InLossAssetCrowdFlow {
|
|
10
|
-
constructor() {
|
|
11
|
-
this.asset_values = {}; // Stores { day1_value_sum: 0, day2_value_sum: 0 }
|
|
12
|
-
this.user_count = 0;
|
|
13
|
-
this.priceMap = null;
|
|
14
|
-
this.mappings = null;
|
|
15
|
-
this.dates = {};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
_initAsset(instrumentId) {
|
|
19
|
-
if (!this.asset_values[instrumentId]) {
|
|
20
|
-
this.asset_values[instrumentId] = { day1_value_sum: 0, day2_value_sum: 0 };
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
process(todayPortfolio, yesterdayPortfolio, userId, context) {
|
|
25
|
-
if (!todayPortfolio || !yesterdayPortfolio || !todayPortfolio.AggregatedPositions || !yesterdayPortfolio.AggregatedPositions) {
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (!this.dates.today && context.todayDateStr && context.yesterdayDateStr) {
|
|
30
|
-
this.dates.today = context.todayDateStr;
|
|
31
|
-
this.dates.yesterday = context.yesterdayDateStr;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const yesterdayPositions = new Map(yesterdayPortfolio.AggregatedPositions.map(p => [p.InstrumentID, p]));
|
|
35
|
-
const todayPositions = new Map(todayPortfolio.AggregatedPositions.map(p => [p.InstrumentID, p]));
|
|
36
|
-
|
|
37
|
-
const allInstrumentIds = new Set([
|
|
38
|
-
...yesterdayPositions.keys(),
|
|
39
|
-
...todayPositions.keys()
|
|
40
|
-
]);
|
|
41
|
-
|
|
42
|
-
for (const instrumentId of allInstrumentIds) {
|
|
43
|
-
const yPos = yesterdayPositions.get(instrumentId);
|
|
44
|
-
const tPos = todayPositions.get(instrumentId);
|
|
45
|
-
|
|
46
|
-
// --- COHORT LOGIC ---
|
|
47
|
-
// Only aggregate if the user is in LOSS on this asset.
|
|
48
|
-
const tNetProfit = tPos?.NetProfit || 0;
|
|
49
|
-
if (tNetProfit >= 0) { // Note: >= 0 (includes zero profit)
|
|
50
|
-
continue; // Skip this asset for this user
|
|
51
|
-
}
|
|
52
|
-
// --- END COHORT LOGIC ---
|
|
53
|
-
|
|
54
|
-
this._initAsset(instrumentId);
|
|
55
|
-
this.asset_values[instrumentId].day1_value_sum += (yPos?.Value || 0);
|
|
56
|
-
this.asset_values[instrumentId].day2_value_sum += (tPos?.Value || 0);
|
|
57
|
-
}
|
|
58
|
-
this.user_count++;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async getResult() {
|
|
62
|
-
if (this.user_count === 0 || !this.dates.today) return {};
|
|
63
|
-
if (!this.priceMap || !this.mappings) {
|
|
64
|
-
const [priceData, mappingData] = await Promise.all([
|
|
65
|
-
loadAllPriceData(),
|
|
66
|
-
loadInstrumentMappings()
|
|
67
|
-
]);
|
|
68
|
-
this.priceMap = priceData;
|
|
69
|
-
this.mappings = mappingData;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const finalResults = {};
|
|
73
|
-
const todayStr = this.dates.today;
|
|
74
|
-
const yesterdayStr = this.dates.yesterday;
|
|
75
|
-
|
|
76
|
-
for (const instrumentId in this.asset_values) {
|
|
77
|
-
const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
|
|
78
|
-
|
|
79
|
-
const avg_day1_value = this.asset_values[instrumentId].day1_value_sum / this.user_count;
|
|
80
|
-
const avg_day2_value = this.asset_values[instrumentId].day2_value_sum / this.user_count;
|
|
81
|
-
const priceChangePct = getDailyPriceChange(instrumentId, yesterdayStr, todayStr, this.priceMap);
|
|
82
|
-
|
|
83
|
-
if (priceChangePct === null) continue;
|
|
84
|
-
|
|
85
|
-
const expected_day2_value = avg_day1_value * (1 + priceChangePct);
|
|
86
|
-
const net_crowd_flow_pct = avg_day2_value - expected_day2_value;
|
|
87
|
-
|
|
88
|
-
finalResults[ticker] = {
|
|
89
|
-
net_crowd_flow_pct: net_crowd_flow_pct,
|
|
90
|
-
avg_value_day1_pct: avg_day1_value,
|
|
91
|
-
avg_value_day2_pct: avg_day2_value
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
return finalResults;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
reset() { /*...reset all properties...*/ }
|
|
98
|
-
}
|
|
1
|
+
const { loadAllPriceData, getDailyPriceChange } = require('../../../utils/price_data_provider');
|
|
2
|
+
const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @fileoverview Calculates "Net Crowd Flow" for each asset, BUT
|
|
6
|
+
* *only* for the cohort of users who are currently IN LOSS
|
|
7
|
+
* on their positions for that asset.
|
|
8
|
+
*/
|
|
9
|
+
class InLossAssetCrowdFlow {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.asset_values = {}; // Stores { day1_value_sum: 0, day2_value_sum: 0 }
|
|
12
|
+
this.user_count = 0;
|
|
13
|
+
this.priceMap = null;
|
|
14
|
+
this.mappings = null;
|
|
15
|
+
this.dates = {};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_initAsset(instrumentId) {
|
|
19
|
+
if (!this.asset_values[instrumentId]) {
|
|
20
|
+
this.asset_values[instrumentId] = { day1_value_sum: 0, day2_value_sum: 0 };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
process(todayPortfolio, yesterdayPortfolio, userId, context) {
|
|
25
|
+
if (!todayPortfolio || !yesterdayPortfolio || !todayPortfolio.AggregatedPositions || !yesterdayPortfolio.AggregatedPositions) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!this.dates.today && context.todayDateStr && context.yesterdayDateStr) {
|
|
30
|
+
this.dates.today = context.todayDateStr;
|
|
31
|
+
this.dates.yesterday = context.yesterdayDateStr;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const yesterdayPositions = new Map(yesterdayPortfolio.AggregatedPositions.map(p => [p.InstrumentID, p]));
|
|
35
|
+
const todayPositions = new Map(todayPortfolio.AggregatedPositions.map(p => [p.InstrumentID, p]));
|
|
36
|
+
|
|
37
|
+
const allInstrumentIds = new Set([
|
|
38
|
+
...yesterdayPositions.keys(),
|
|
39
|
+
...todayPositions.keys()
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
for (const instrumentId of allInstrumentIds) {
|
|
43
|
+
const yPos = yesterdayPositions.get(instrumentId);
|
|
44
|
+
const tPos = todayPositions.get(instrumentId);
|
|
45
|
+
|
|
46
|
+
// --- COHORT LOGIC ---
|
|
47
|
+
// Only aggregate if the user is in LOSS on this asset.
|
|
48
|
+
const tNetProfit = tPos?.NetProfit || 0;
|
|
49
|
+
if (tNetProfit >= 0) { // Note: >= 0 (includes zero profit)
|
|
50
|
+
continue; // Skip this asset for this user
|
|
51
|
+
}
|
|
52
|
+
// --- END COHORT LOGIC ---
|
|
53
|
+
|
|
54
|
+
this._initAsset(instrumentId);
|
|
55
|
+
this.asset_values[instrumentId].day1_value_sum += (yPos?.Value || 0);
|
|
56
|
+
this.asset_values[instrumentId].day2_value_sum += (tPos?.Value || 0);
|
|
57
|
+
}
|
|
58
|
+
this.user_count++;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async getResult() {
|
|
62
|
+
if (this.user_count === 0 || !this.dates.today) return {};
|
|
63
|
+
if (!this.priceMap || !this.mappings) {
|
|
64
|
+
const [priceData, mappingData] = await Promise.all([
|
|
65
|
+
loadAllPriceData(),
|
|
66
|
+
loadInstrumentMappings()
|
|
67
|
+
]);
|
|
68
|
+
this.priceMap = priceData;
|
|
69
|
+
this.mappings = mappingData;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const finalResults = {};
|
|
73
|
+
const todayStr = this.dates.today;
|
|
74
|
+
const yesterdayStr = this.dates.yesterday;
|
|
75
|
+
|
|
76
|
+
for (const instrumentId in this.asset_values) {
|
|
77
|
+
const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
|
|
78
|
+
|
|
79
|
+
const avg_day1_value = this.asset_values[instrumentId].day1_value_sum / this.user_count;
|
|
80
|
+
const avg_day2_value = this.asset_values[instrumentId].day2_value_sum / this.user_count;
|
|
81
|
+
const priceChangePct = getDailyPriceChange(instrumentId, yesterdayStr, todayStr, this.priceMap);
|
|
82
|
+
|
|
83
|
+
if (priceChangePct === null) continue;
|
|
84
|
+
|
|
85
|
+
const expected_day2_value = avg_day1_value * (1 + priceChangePct);
|
|
86
|
+
const net_crowd_flow_pct = avg_day2_value - expected_day2_value;
|
|
87
|
+
|
|
88
|
+
finalResults[ticker] = {
|
|
89
|
+
net_crowd_flow_pct: net_crowd_flow_pct,
|
|
90
|
+
avg_value_day1_pct: avg_day1_value,
|
|
91
|
+
avg_value_day2_pct: avg_day2_value
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return finalResults;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
reset() { /*...reset all properties...*/ }
|
|
98
|
+
}
|
|
99
99
|
module.exports = InLossAssetCrowdFlow;
|
|
@@ -1,100 +1,100 @@
|
|
|
1
|
-
const { loadAllPriceData, getDailyPriceChange } = require('../../../utils/price_data_provider');
|
|
2
|
-
const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* @fileoverview Calculates "Net Crowd Flow" for each asset, BUT
|
|
6
|
-
* *only* for the cohort of users who are currently IN PROFIT
|
|
7
|
-
* on their positions for that asset.
|
|
8
|
-
*/
|
|
9
|
-
class InProfitAssetCrowdFlow {
|
|
10
|
-
constructor() {
|
|
11
|
-
this.asset_values = {}; // Stores { day1_value_sum: 0, day2_value_sum: 0 }
|
|
12
|
-
this.user_count = 0;
|
|
13
|
-
this.priceMap = null;
|
|
14
|
-
this.mappings = null;
|
|
15
|
-
this.dates = {};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
_initAsset(instrumentId) {
|
|
19
|
-
if (!this.asset_values[instrumentId]) {
|
|
20
|
-
this.asset_values[instrumentId] = { day1_value_sum: 0, day2_value_sum: 0 };
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
process(todayPortfolio, yesterdayPortfolio, userId, context) {
|
|
25
|
-
if (!todayPortfolio || !yesterdayPortfolio || !todayPortfolio.AggregatedPositions || !yesterdayPortfolio.AggregatedPositions) {
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (!this.dates.today && context.todayDateStr && context.yesterdayDateStr) {
|
|
30
|
-
this.dates.today = context.todayDateStr;
|
|
31
|
-
this.dates.yesterday = context.yesterdayDateStr;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const yesterdayPositions = new Map(yesterdayPortfolio.AggregatedPositions.map(p => [p.InstrumentID, p]));
|
|
35
|
-
const todayPositions = new Map(todayPortfolio.AggregatedPositions.map(p => [p.InstrumentID, p]));
|
|
36
|
-
|
|
37
|
-
const allInstrumentIds = new Set([
|
|
38
|
-
...yesterdayPositions.keys(),
|
|
39
|
-
...todayPositions.keys()
|
|
40
|
-
]);
|
|
41
|
-
|
|
42
|
-
for (const instrumentId of allInstrumentIds) {
|
|
43
|
-
const yPos = yesterdayPositions.get(instrumentId);
|
|
44
|
-
const tPos = todayPositions.get(instrumentId);
|
|
45
|
-
|
|
46
|
-
// --- COHORT LOGIC ---
|
|
47
|
-
// Only aggregate if the user is in PROFIT on this asset.
|
|
48
|
-
// We check *today's* profit status as the primary signal.
|
|
49
|
-
const tNetProfit = tPos?.NetProfit || 0;
|
|
50
|
-
if (tNetProfit <= 0) {
|
|
51
|
-
continue; // Skip this asset for this user
|
|
52
|
-
}
|
|
53
|
-
// --- END COHORT LOGIC ---
|
|
54
|
-
|
|
55
|
-
this._initAsset(instrumentId);
|
|
56
|
-
this.asset_values[instrumentId].day1_value_sum += (yPos?.Value || 0);
|
|
57
|
-
this.asset_values[instrumentId].day2_value_sum += (tPos?.Value || 0);
|
|
58
|
-
}
|
|
59
|
-
this.user_count++; // Note: This is user_count of *all* users, which is fine for avg.
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async getResult() {
|
|
63
|
-
if (this.user_count === 0 || !this.dates.today) return {};
|
|
64
|
-
if (!this.priceMap || !this.mappings) {
|
|
65
|
-
const [priceData, mappingData] = await Promise.all([
|
|
66
|
-
loadAllPriceData(),
|
|
67
|
-
loadInstrumentMappings()
|
|
68
|
-
]);
|
|
69
|
-
this.priceMap = priceData;
|
|
70
|
-
this.mappings = mappingData;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const finalResults = {};
|
|
74
|
-
const todayStr = this.dates.today;
|
|
75
|
-
const yesterdayStr = this.dates.yesterday;
|
|
76
|
-
|
|
77
|
-
for (const instrumentId in this.asset_values) {
|
|
78
|
-
const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
|
|
79
|
-
|
|
80
|
-
const avg_day1_value = this.asset_values[instrumentId].day1_value_sum / this.user_count;
|
|
81
|
-
const avg_day2_value = this.asset_values[instrumentId].day2_value_sum / this.user_count;
|
|
82
|
-
const priceChangePct = getDailyPriceChange(instrumentId, yesterdayStr, todayStr, this.priceMap);
|
|
83
|
-
|
|
84
|
-
if (priceChangePct === null) continue;
|
|
85
|
-
|
|
86
|
-
const expected_day2_value = avg_day1_value * (1 + priceChangePct);
|
|
87
|
-
const net_crowd_flow_pct = avg_day2_value - expected_day2_value;
|
|
88
|
-
|
|
89
|
-
finalResults[ticker] = {
|
|
90
|
-
net_crowd_flow_pct: net_crowd_flow_pct,
|
|
91
|
-
avg_value_day1_pct: avg_day1_value,
|
|
92
|
-
avg_value_day2_pct: avg_day2_value
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
return finalResults;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
reset() { /*...reset all properties...*/ }
|
|
99
|
-
}
|
|
1
|
+
const { loadAllPriceData, getDailyPriceChange } = require('../../../utils/price_data_provider');
|
|
2
|
+
const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @fileoverview Calculates "Net Crowd Flow" for each asset, BUT
|
|
6
|
+
* *only* for the cohort of users who are currently IN PROFIT
|
|
7
|
+
* on their positions for that asset.
|
|
8
|
+
*/
|
|
9
|
+
class InProfitAssetCrowdFlow {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.asset_values = {}; // Stores { day1_value_sum: 0, day2_value_sum: 0 }
|
|
12
|
+
this.user_count = 0;
|
|
13
|
+
this.priceMap = null;
|
|
14
|
+
this.mappings = null;
|
|
15
|
+
this.dates = {};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_initAsset(instrumentId) {
|
|
19
|
+
if (!this.asset_values[instrumentId]) {
|
|
20
|
+
this.asset_values[instrumentId] = { day1_value_sum: 0, day2_value_sum: 0 };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
process(todayPortfolio, yesterdayPortfolio, userId, context) {
|
|
25
|
+
if (!todayPortfolio || !yesterdayPortfolio || !todayPortfolio.AggregatedPositions || !yesterdayPortfolio.AggregatedPositions) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!this.dates.today && context.todayDateStr && context.yesterdayDateStr) {
|
|
30
|
+
this.dates.today = context.todayDateStr;
|
|
31
|
+
this.dates.yesterday = context.yesterdayDateStr;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const yesterdayPositions = new Map(yesterdayPortfolio.AggregatedPositions.map(p => [p.InstrumentID, p]));
|
|
35
|
+
const todayPositions = new Map(todayPortfolio.AggregatedPositions.map(p => [p.InstrumentID, p]));
|
|
36
|
+
|
|
37
|
+
const allInstrumentIds = new Set([
|
|
38
|
+
...yesterdayPositions.keys(),
|
|
39
|
+
...todayPositions.keys()
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
for (const instrumentId of allInstrumentIds) {
|
|
43
|
+
const yPos = yesterdayPositions.get(instrumentId);
|
|
44
|
+
const tPos = todayPositions.get(instrumentId);
|
|
45
|
+
|
|
46
|
+
// --- COHORT LOGIC ---
|
|
47
|
+
// Only aggregate if the user is in PROFIT on this asset.
|
|
48
|
+
// We check *today's* profit status as the primary signal.
|
|
49
|
+
const tNetProfit = tPos?.NetProfit || 0;
|
|
50
|
+
if (tNetProfit <= 0) {
|
|
51
|
+
continue; // Skip this asset for this user
|
|
52
|
+
}
|
|
53
|
+
// --- END COHORT LOGIC ---
|
|
54
|
+
|
|
55
|
+
this._initAsset(instrumentId);
|
|
56
|
+
this.asset_values[instrumentId].day1_value_sum += (yPos?.Value || 0);
|
|
57
|
+
this.asset_values[instrumentId].day2_value_sum += (tPos?.Value || 0);
|
|
58
|
+
}
|
|
59
|
+
this.user_count++; // Note: This is user_count of *all* users, which is fine for avg.
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async getResult() {
|
|
63
|
+
if (this.user_count === 0 || !this.dates.today) return {};
|
|
64
|
+
if (!this.priceMap || !this.mappings) {
|
|
65
|
+
const [priceData, mappingData] = await Promise.all([
|
|
66
|
+
loadAllPriceData(),
|
|
67
|
+
loadInstrumentMappings()
|
|
68
|
+
]);
|
|
69
|
+
this.priceMap = priceData;
|
|
70
|
+
this.mappings = mappingData;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const finalResults = {};
|
|
74
|
+
const todayStr = this.dates.today;
|
|
75
|
+
const yesterdayStr = this.dates.yesterday;
|
|
76
|
+
|
|
77
|
+
for (const instrumentId in this.asset_values) {
|
|
78
|
+
const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
|
|
79
|
+
|
|
80
|
+
const avg_day1_value = this.asset_values[instrumentId].day1_value_sum / this.user_count;
|
|
81
|
+
const avg_day2_value = this.asset_values[instrumentId].day2_value_sum / this.user_count;
|
|
82
|
+
const priceChangePct = getDailyPriceChange(instrumentId, yesterdayStr, todayStr, this.priceMap);
|
|
83
|
+
|
|
84
|
+
if (priceChangePct === null) continue;
|
|
85
|
+
|
|
86
|
+
const expected_day2_value = avg_day1_value * (1 + priceChangePct);
|
|
87
|
+
const net_crowd_flow_pct = avg_day2_value - expected_day2_value;
|
|
88
|
+
|
|
89
|
+
finalResults[ticker] = {
|
|
90
|
+
net_crowd_flow_pct: net_crowd_flow_pct,
|
|
91
|
+
avg_value_day1_pct: avg_day1_value,
|
|
92
|
+
avg_value_day2_pct: avg_day2_value
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return finalResults;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
reset() { /*...reset all properties...*/ }
|
|
99
|
+
}
|
|
100
100
|
module.exports = InProfitAssetCrowdFlow;
|
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Calculates the "Paper Hands vs. Diamond Hands" index.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
class PaperVsDiamondHands {
|
|
6
|
-
constructor() {
|
|
7
|
-
this.newPositions = 0;
|
|
8
|
-
this.closedPositions = 0;
|
|
9
|
-
this.heldPositions = 0;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
process(todayPortfolio, yesterdayPortfolio, userId) {
|
|
13
|
-
if (!todayPortfolio || !yesterdayPortfolio) {
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const todayIds = new Set((todayPortfolio.PublicPositions || todayPortfolio.AggregatedPositions).map(p => p.PositionID || p.InstrumentID));
|
|
18
|
-
const yesterdayIds = new Set((yesterdayPortfolio.PublicPositions || yesterdayPortfolio.AggregatedPositions).map(p => p.PositionID || p.InstrumentID));
|
|
19
|
-
|
|
20
|
-
const newPos = [...todayIds].filter(id => !yesterdayIds.has(id)).length;
|
|
21
|
-
const closedPos = [...yesterdayIds].filter(id => !todayIds.has(id)).length;
|
|
22
|
-
const heldPos = [...todayIds].filter(id => yesterdayIds.has(id)).length;
|
|
23
|
-
|
|
24
|
-
this.newPositions += newPos;
|
|
25
|
-
this.closedPositions += closedPos;
|
|
26
|
-
this.heldPositions += heldPos;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
getResult() {
|
|
30
|
-
const totalPositions = this.newPositions + this.closedPositions + this.heldPositions;
|
|
31
|
-
if (totalPositions === 0) return {};
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
paper_hands_index: (this.closedPositions / totalPositions) * 100, // High turnover
|
|
35
|
-
diamond_hands_index: (this.heldPositions / totalPositions) * 100, // Low turnover
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Calculates the "Paper Hands vs. Diamond Hands" index.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
class PaperVsDiamondHands {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.newPositions = 0;
|
|
8
|
+
this.closedPositions = 0;
|
|
9
|
+
this.heldPositions = 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
process(todayPortfolio, yesterdayPortfolio, userId) {
|
|
13
|
+
if (!todayPortfolio || !yesterdayPortfolio) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const todayIds = new Set((todayPortfolio.PublicPositions || todayPortfolio.AggregatedPositions).map(p => p.PositionID || p.InstrumentID));
|
|
18
|
+
const yesterdayIds = new Set((yesterdayPortfolio.PublicPositions || yesterdayPortfolio.AggregatedPositions).map(p => p.PositionID || p.InstrumentID));
|
|
19
|
+
|
|
20
|
+
const newPos = [...todayIds].filter(id => !yesterdayIds.has(id)).length;
|
|
21
|
+
const closedPos = [...yesterdayIds].filter(id => !todayIds.has(id)).length;
|
|
22
|
+
const heldPos = [...todayIds].filter(id => yesterdayIds.has(id)).length;
|
|
23
|
+
|
|
24
|
+
this.newPositions += newPos;
|
|
25
|
+
this.closedPositions += closedPos;
|
|
26
|
+
this.heldPositions += heldPos;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
getResult() {
|
|
30
|
+
const totalPositions = this.newPositions + this.closedPositions + this.heldPositions;
|
|
31
|
+
if (totalPositions === 0) return {};
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
paper_hands_index: (this.closedPositions / totalPositions) * 100, // High turnover
|
|
35
|
+
diamond_hands_index: (this.heldPositions / totalPositions) * 100, // Low turnover
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
40
|
module.exports = PaperVsDiamondHands;
|
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Aggregates P/L by the number of positions a user holds.
|
|
3
|
-
* Used to create a dot plot.
|
|
4
|
-
*/
|
|
5
|
-
class PositionCountPnl {
|
|
6
|
-
constructor() {
|
|
7
|
-
// We will store sums and counts to calculate averages later
|
|
8
|
-
this.pnl_by_position_count = {};
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
_initBucket(count) {
|
|
12
|
-
if (!this.pnl_by_position_count[count]) {
|
|
13
|
-
this.pnl_by_position_count[count] = { pnl_sum: 0, count: 0 };
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* FIX: Helper function to calculate total P&L from positions
|
|
19
|
-
* @param {object} portfolio
|
|
20
|
-
* @returns {number|null}
|
|
21
|
-
*/
|
|
22
|
-
_calculateTotalPnl(portfolio) {
|
|
23
|
-
const positions = portfolio?.AggregatedPositions || portfolio?.PublicPositions;
|
|
24
|
-
if (positions && Array.isArray(positions)) {
|
|
25
|
-
// Sum all NetProfit fields, defaulting to 0 if a position has no NetProfit
|
|
26
|
-
return positions.reduce((sum, pos) => sum + (pos.NetProfit || 0), 0);
|
|
27
|
-
}
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
process(todayPortfolio, yesterdayPortfolio, userId) {
|
|
32
|
-
// FIX: Only need todayPortfolio for this logic
|
|
33
|
-
if (!todayPortfolio) {
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const positions = todayPortfolio.AggregatedPositions || todayPortfolio.PublicPositions;
|
|
38
|
-
|
|
39
|
-
if (!positions || !Array.isArray(positions)) {
|
|
40
|
-
return; // Skip users with no positions array
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const positionCount = positions.length;
|
|
44
|
-
if (positionCount === 0) {
|
|
45
|
-
return; // Skip users with no positions
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// FIX: Calculate dailyPnl by summing NetProfit from all positions
|
|
49
|
-
const dailyPnl = this._calculateTotalPnl(todayPortfolio);
|
|
50
|
-
|
|
51
|
-
if (dailyPnl === null) {
|
|
52
|
-
return; // Cannot calculate P&L for this user
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
this._initBucket(positionCount);
|
|
56
|
-
this.pnl_by_position_count[positionCount].pnl_sum += dailyPnl;
|
|
57
|
-
this.pnl_by_position_count[positionCount].count++;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
getResult() {
|
|
61
|
-
// Return the aggregated object.
|
|
62
|
-
// Frontend will iterate keys, calculate avg (pnl_sum/count),
|
|
63
|
-
// and plot { x: positionCount, y: avg_pnl }
|
|
64
|
-
return this.pnl_by_position_count;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Aggregates P/L by the number of positions a user holds.
|
|
3
|
+
* Used to create a dot plot.
|
|
4
|
+
*/
|
|
5
|
+
class PositionCountPnl {
|
|
6
|
+
constructor() {
|
|
7
|
+
// We will store sums and counts to calculate averages later
|
|
8
|
+
this.pnl_by_position_count = {};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
_initBucket(count) {
|
|
12
|
+
if (!this.pnl_by_position_count[count]) {
|
|
13
|
+
this.pnl_by_position_count[count] = { pnl_sum: 0, count: 0 };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* FIX: Helper function to calculate total P&L from positions
|
|
19
|
+
* @param {object} portfolio
|
|
20
|
+
* @returns {number|null}
|
|
21
|
+
*/
|
|
22
|
+
_calculateTotalPnl(portfolio) {
|
|
23
|
+
const positions = portfolio?.AggregatedPositions || portfolio?.PublicPositions;
|
|
24
|
+
if (positions && Array.isArray(positions)) {
|
|
25
|
+
// Sum all NetProfit fields, defaulting to 0 if a position has no NetProfit
|
|
26
|
+
return positions.reduce((sum, pos) => sum + (pos.NetProfit || 0), 0);
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
process(todayPortfolio, yesterdayPortfolio, userId) {
|
|
32
|
+
// FIX: Only need todayPortfolio for this logic
|
|
33
|
+
if (!todayPortfolio) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const positions = todayPortfolio.AggregatedPositions || todayPortfolio.PublicPositions;
|
|
38
|
+
|
|
39
|
+
if (!positions || !Array.isArray(positions)) {
|
|
40
|
+
return; // Skip users with no positions array
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const positionCount = positions.length;
|
|
44
|
+
if (positionCount === 0) {
|
|
45
|
+
return; // Skip users with no positions
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// FIX: Calculate dailyPnl by summing NetProfit from all positions
|
|
49
|
+
const dailyPnl = this._calculateTotalPnl(todayPortfolio);
|
|
50
|
+
|
|
51
|
+
if (dailyPnl === null) {
|
|
52
|
+
return; // Cannot calculate P&L for this user
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this._initBucket(positionCount);
|
|
56
|
+
this.pnl_by_position_count[positionCount].pnl_sum += dailyPnl;
|
|
57
|
+
this.pnl_by_position_count[positionCount].count++;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getResult() {
|
|
61
|
+
// Return the aggregated object.
|
|
62
|
+
// Frontend will iterate keys, calculate avg (pnl_sum/count),
|
|
63
|
+
// and plot { x: positionCount, y: avg_pnl }
|
|
64
|
+
return this.pnl_by_position_count;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
68
|
module.exports = PositionCountPnl;
|