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
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Analyzes sector rotation by comparing investment amounts between two days.
|
|
3
|
-
*/
|
|
4
|
-
// CORRECTED PATH: ../utils/ instead of ../../utils/
|
|
5
|
-
const { getInstrumentSectorMap } = require('../../../utils/sector_mapping_provider');
|
|
6
|
-
|
|
7
|
-
class SectorRotation {
|
|
8
|
-
// ... (rest of the code is unchanged) ...
|
|
9
|
-
constructor() {
|
|
10
|
-
this.todaySectorInvestment = {};
|
|
11
|
-
this.yesterdaySectorInvestment = {};
|
|
12
|
-
this.sectorMap = null;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
async process(todayPortfolio, yesterdayPortfolio, userId) {
|
|
16
|
-
if (!this.sectorMap) {
|
|
17
|
-
this.sectorMap = await getInstrumentSectorMap();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (todayPortfolio) {
|
|
21
|
-
this.accumulateSectorInvestment(todayPortfolio, this.todaySectorInvestment);
|
|
22
|
-
}
|
|
23
|
-
if (yesterdayPortfolio) {
|
|
24
|
-
this.accumulateSectorInvestment(yesterdayPortfolio, this.yesterdaySectorInvestment);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
accumulateSectorInvestment(portfolio, target) {
|
|
29
|
-
// Use AggregatedPositions, as PublicPositions don't have Invested amount
|
|
30
|
-
if (portfolio && portfolio.AggregatedPositions) {
|
|
31
|
-
for (const pos of portfolio.AggregatedPositions) {
|
|
32
|
-
// Check if sectorMap is loaded before accessing it
|
|
33
|
-
if (!this.sectorMap) {
|
|
34
|
-
console.warn('Sector map not loaded in accumulateSectorInvestment');
|
|
35
|
-
continue; // or handle appropriately
|
|
36
|
-
}
|
|
37
|
-
const sector = this.sectorMap[pos.InstrumentID] || 'N/A';
|
|
38
|
-
if (!target[sector]) {
|
|
39
|
-
target[sector] = 0;
|
|
40
|
-
}
|
|
41
|
-
// Use Invested amount if available, otherwise fallback might be needed
|
|
42
|
-
target[sector] += (pos.Invested || pos.Amount || 0); // Added fallback
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
// Optional: Handle PublicPositions if needed, though they lack 'Invested'
|
|
46
|
-
else if (portfolio && portfolio.PublicPositions) {
|
|
47
|
-
// Logic to handle PublicPositions if necessary, e.g., using 'Amount'
|
|
48
|
-
// console.warn("Using PublicPositions for sector rotation, 'Invested' amount missing.");
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
getResult() {
|
|
54
|
-
const sectorRotationData = {};
|
|
55
|
-
const allSectors = new Set([...Object.keys(this.todaySectorInvestment), ...Object.keys(this.yesterdaySectorInvestment)]);
|
|
56
|
-
|
|
57
|
-
for (const sector of allSectors) {
|
|
58
|
-
const todayAmount = this.todaySectorInvestment[sector] || 0;
|
|
59
|
-
const yesterdayAmount = this.yesterdaySectorInvestment[sector] || 0;
|
|
60
|
-
sectorRotationData[sector] = todayAmount - yesterdayAmount;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return { sector_rotation: sectorRotationData };
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Analyzes sector rotation by comparing investment amounts between two days.
|
|
3
|
+
*/
|
|
4
|
+
// CORRECTED PATH: ../utils/ instead of ../../utils/
|
|
5
|
+
const { getInstrumentSectorMap } = require('../../../utils/sector_mapping_provider');
|
|
6
|
+
|
|
7
|
+
class SectorRotation {
|
|
8
|
+
// ... (rest of the code is unchanged) ...
|
|
9
|
+
constructor() {
|
|
10
|
+
this.todaySectorInvestment = {};
|
|
11
|
+
this.yesterdaySectorInvestment = {};
|
|
12
|
+
this.sectorMap = null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async process(todayPortfolio, yesterdayPortfolio, userId) {
|
|
16
|
+
if (!this.sectorMap) {
|
|
17
|
+
this.sectorMap = await getInstrumentSectorMap();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (todayPortfolio) {
|
|
21
|
+
this.accumulateSectorInvestment(todayPortfolio, this.todaySectorInvestment);
|
|
22
|
+
}
|
|
23
|
+
if (yesterdayPortfolio) {
|
|
24
|
+
this.accumulateSectorInvestment(yesterdayPortfolio, this.yesterdaySectorInvestment);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
accumulateSectorInvestment(portfolio, target) {
|
|
29
|
+
// Use AggregatedPositions, as PublicPositions don't have Invested amount
|
|
30
|
+
if (portfolio && portfolio.AggregatedPositions) {
|
|
31
|
+
for (const pos of portfolio.AggregatedPositions) {
|
|
32
|
+
// Check if sectorMap is loaded before accessing it
|
|
33
|
+
if (!this.sectorMap) {
|
|
34
|
+
console.warn('Sector map not loaded in accumulateSectorInvestment');
|
|
35
|
+
continue; // or handle appropriately
|
|
36
|
+
}
|
|
37
|
+
const sector = this.sectorMap[pos.InstrumentID] || 'N/A';
|
|
38
|
+
if (!target[sector]) {
|
|
39
|
+
target[sector] = 0;
|
|
40
|
+
}
|
|
41
|
+
// Use Invested amount if available, otherwise fallback might be needed
|
|
42
|
+
target[sector] += (pos.Invested || pos.Amount || 0); // Added fallback
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Optional: Handle PublicPositions if needed, though they lack 'Invested'
|
|
46
|
+
else if (portfolio && portfolio.PublicPositions) {
|
|
47
|
+
// Logic to handle PublicPositions if necessary, e.g., using 'Amount'
|
|
48
|
+
// console.warn("Using PublicPositions for sector rotation, 'Invested' amount missing.");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
getResult() {
|
|
54
|
+
const sectorRotationData = {};
|
|
55
|
+
const allSectors = new Set([...Object.keys(this.todaySectorInvestment), ...Object.keys(this.yesterdaySectorInvestment)]);
|
|
56
|
+
|
|
57
|
+
for (const sector of allSectors) {
|
|
58
|
+
const todayAmount = this.todaySectorInvestment[sector] || 0;
|
|
59
|
+
const yesterdayAmount = this.yesterdaySectorInvestment[sector] || 0;
|
|
60
|
+
sectorRotationData[sector] = todayAmount - yesterdayAmount;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { sector_rotation: sectorRotationData };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
68
|
module.exports = SectorRotation;
|
|
@@ -1,81 +1,81 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Calculates a "Crowd Conviction" score for each instrument.
|
|
3
|
-
*/
|
|
4
|
-
// CORRECTED PATH: ../utils/ instead of ../../utils/
|
|
5
|
-
const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class CrowdConvictionScore {
|
|
9
|
-
// ... (rest of the code is unchanged) ...
|
|
10
|
-
constructor() {
|
|
11
|
-
this.convictionData = {};
|
|
12
|
-
this.mappings = null;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
async process(todayPortfolio, yesterdayPortfolio, userId) {
|
|
16
|
-
if (!todayPortfolio) return;
|
|
17
|
-
|
|
18
|
-
if(!this.mappings) {
|
|
19
|
-
this.mappings = await loadInstrumentMappings();
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const positions = todayPortfolio.PublicPositions || todayPortfolio.AggregatedPositions;
|
|
23
|
-
if (!positions) return;
|
|
24
|
-
|
|
25
|
-
for (const pos of positions) {
|
|
26
|
-
const instrumentId = pos.InstrumentID;
|
|
27
|
-
const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId.toString();
|
|
28
|
-
|
|
29
|
-
if (!this.convictionData[ticker]) {
|
|
30
|
-
this.convictionData[ticker] = {
|
|
31
|
-
totalScore: 0,
|
|
32
|
-
count: 0,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
let score = 0;
|
|
37
|
-
let weights = 0;
|
|
38
|
-
|
|
39
|
-
if (pos.OpenDateTime) {
|
|
40
|
-
const openTime = new Date(pos.OpenDateTime);
|
|
41
|
-
const holdingHours = (new Date() - openTime) / (1000 * 60 * 60);
|
|
42
|
-
score += Math.log(holdingHours + 1);
|
|
43
|
-
weights += 1;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (pos.StopLossRate > 0 && pos.TakeProfitRate > 0) {
|
|
47
|
-
const risk = Math.abs(pos.OpenRate - pos.StopLossRate);
|
|
48
|
-
const reward = Math.abs(pos.TakeProfitRate - pos.OpenRate);
|
|
49
|
-
if (risk > 0) {
|
|
50
|
-
const ratio = reward / risk;
|
|
51
|
-
score += ratio * 0.5;
|
|
52
|
-
weights += 0.5;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (pos.Leverage > 1) {
|
|
57
|
-
score += Math.log(pos.Leverage) * 1.5;
|
|
58
|
-
weights += 1.5;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (weights > 0) {
|
|
62
|
-
const finalScore = (score / weights) * 100;
|
|
63
|
-
this.convictionData[ticker].totalScore += finalScore;
|
|
64
|
-
this.convictionData[ticker].count++;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
getResult() {
|
|
70
|
-
const result = {};
|
|
71
|
-
for (const ticker in this.convictionData) {
|
|
72
|
-
const data = this.convictionData[ticker];
|
|
73
|
-
if (data.count > 0) {
|
|
74
|
-
result[ticker] = data.totalScore / data.count;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return { crowd_conviction_score: result };
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Calculates a "Crowd Conviction" score for each instrument.
|
|
3
|
+
*/
|
|
4
|
+
// CORRECTED PATH: ../utils/ instead of ../../utils/
|
|
5
|
+
const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CrowdConvictionScore {
|
|
9
|
+
// ... (rest of the code is unchanged) ...
|
|
10
|
+
constructor() {
|
|
11
|
+
this.convictionData = {};
|
|
12
|
+
this.mappings = null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async process(todayPortfolio, yesterdayPortfolio, userId) {
|
|
16
|
+
if (!todayPortfolio) return;
|
|
17
|
+
|
|
18
|
+
if(!this.mappings) {
|
|
19
|
+
this.mappings = await loadInstrumentMappings();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const positions = todayPortfolio.PublicPositions || todayPortfolio.AggregatedPositions;
|
|
23
|
+
if (!positions) return;
|
|
24
|
+
|
|
25
|
+
for (const pos of positions) {
|
|
26
|
+
const instrumentId = pos.InstrumentID;
|
|
27
|
+
const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId.toString();
|
|
28
|
+
|
|
29
|
+
if (!this.convictionData[ticker]) {
|
|
30
|
+
this.convictionData[ticker] = {
|
|
31
|
+
totalScore: 0,
|
|
32
|
+
count: 0,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let score = 0;
|
|
37
|
+
let weights = 0;
|
|
38
|
+
|
|
39
|
+
if (pos.OpenDateTime) {
|
|
40
|
+
const openTime = new Date(pos.OpenDateTime);
|
|
41
|
+
const holdingHours = (new Date() - openTime) / (1000 * 60 * 60);
|
|
42
|
+
score += Math.log(holdingHours + 1);
|
|
43
|
+
weights += 1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (pos.StopLossRate > 0 && pos.TakeProfitRate > 0) {
|
|
47
|
+
const risk = Math.abs(pos.OpenRate - pos.StopLossRate);
|
|
48
|
+
const reward = Math.abs(pos.TakeProfitRate - pos.OpenRate);
|
|
49
|
+
if (risk > 0) {
|
|
50
|
+
const ratio = reward / risk;
|
|
51
|
+
score += ratio * 0.5;
|
|
52
|
+
weights += 0.5;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (pos.Leverage > 1) {
|
|
57
|
+
score += Math.log(pos.Leverage) * 1.5;
|
|
58
|
+
weights += 1.5;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (weights > 0) {
|
|
62
|
+
const finalScore = (score / weights) * 100;
|
|
63
|
+
this.convictionData[ticker].totalScore += finalScore;
|
|
64
|
+
this.convictionData[ticker].count++;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
getResult() {
|
|
70
|
+
const result = {};
|
|
71
|
+
for (const ticker in this.convictionData) {
|
|
72
|
+
const data = this.convictionData[ticker];
|
|
73
|
+
if (data.count > 0) {
|
|
74
|
+
result[ticker] = data.totalScore / data.count;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { crowd_conviction_score: result };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
81
|
module.exports = CrowdConvictionScore;
|
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Aggregates the total number of posts per asset (ticker).
|
|
3
|
-
* This data can be used for a time-series trend (AssetPostsTrend) or
|
|
4
|
-
* for a "Top Tickers" card.
|
|
5
|
-
* This calculation ONLY uses the social post insights context.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
class SocialAssetPostsTrend {
|
|
9
|
-
constructor() {
|
|
10
|
-
this.postsByTicker = {};
|
|
11
|
-
this.processed = false; // Flag to ensure this runs only once
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* @param {null} todayPortfolio - Not used.
|
|
16
|
-
* ... (other params not used) ...
|
|
17
|
-
* @param {object} todaySocialPostInsights - Map of { [postId]: postData } for today.
|
|
18
|
-
* @param {object} yesterdaySocialPostInsights - Not used.
|
|
19
|
-
*/
|
|
20
|
-
async process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights, todaySocialPostInsights, yesterdaySocialPostInsights) {
|
|
21
|
-
|
|
22
|
-
if (this.processed || !todaySocialPostInsights) {
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
this.processed = true;
|
|
26
|
-
|
|
27
|
-
const posts = Object.values(todaySocialPostInsights);
|
|
28
|
-
|
|
29
|
-
for (const post of posts) {
|
|
30
|
-
if (!post.tickers || !Array.isArray(post.tickers)) continue;
|
|
31
|
-
|
|
32
|
-
for (const ticker of post.tickers) {
|
|
33
|
-
// Ensure ticker is a string and valid
|
|
34
|
-
const validTicker = String(ticker).trim();
|
|
35
|
-
if (validTicker) {
|
|
36
|
-
this.postsByTicker[validTicker] = (this.postsByTicker[validTicker] || 0) + 1;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async getResult() {
|
|
43
|
-
// Returns an object like: { "AAPL": 15, "TSLA": 22 }
|
|
44
|
-
return this.postsByTicker;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
reset() {
|
|
48
|
-
this.postsByTicker = {};
|
|
49
|
-
this.processed = false;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Aggregates the total number of posts per asset (ticker).
|
|
3
|
+
* This data can be used for a time-series trend (AssetPostsTrend) or
|
|
4
|
+
* for a "Top Tickers" card.
|
|
5
|
+
* This calculation ONLY uses the social post insights context.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
class SocialAssetPostsTrend {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.postsByTicker = {};
|
|
11
|
+
this.processed = false; // Flag to ensure this runs only once
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {null} todayPortfolio - Not used.
|
|
16
|
+
* ... (other params not used) ...
|
|
17
|
+
* @param {object} todaySocialPostInsights - Map of { [postId]: postData } for today.
|
|
18
|
+
* @param {object} yesterdaySocialPostInsights - Not used.
|
|
19
|
+
*/
|
|
20
|
+
async process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights, todaySocialPostInsights, yesterdaySocialPostInsights) {
|
|
21
|
+
|
|
22
|
+
if (this.processed || !todaySocialPostInsights) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
this.processed = true;
|
|
26
|
+
|
|
27
|
+
const posts = Object.values(todaySocialPostInsights);
|
|
28
|
+
|
|
29
|
+
for (const post of posts) {
|
|
30
|
+
if (!post.tickers || !Array.isArray(post.tickers)) continue;
|
|
31
|
+
|
|
32
|
+
for (const ticker of post.tickers) {
|
|
33
|
+
// Ensure ticker is a string and valid
|
|
34
|
+
const validTicker = String(ticker).trim();
|
|
35
|
+
if (validTicker) {
|
|
36
|
+
this.postsByTicker[validTicker] = (this.postsByTicker[validTicker] || 0) + 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async getResult() {
|
|
43
|
+
// Returns an object like: { "AAPL": 15, "TSLA": 22 }
|
|
44
|
+
return this.postsByTicker;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
reset() {
|
|
48
|
+
this.postsByTicker = {};
|
|
49
|
+
this.processed = false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
53
|
module.exports = SocialAssetPostsTrend;
|
|
@@ -1,103 +1,103 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Finds the top mentioned words in all posts for the day,
|
|
3
|
-
* filtering out common stop words, and calculates the trend from yesterday.
|
|
4
|
-
* This calculation ONLY uses the social post insights context.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
class SocialTopMentionedWords {
|
|
8
|
-
constructor() {
|
|
9
|
-
this.todayWordCounts = {};
|
|
10
|
-
this.yesterdayWordCounts = {};
|
|
11
|
-
this.processed = false;
|
|
12
|
-
|
|
13
|
-
// A small, representative list of stop words.
|
|
14
|
-
// In a real implementation, this list would be much larger.
|
|
15
|
-
this.stopWords = new Set([
|
|
16
|
-
'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
|
17
|
-
'have', 'has', 'had', 'do', 'does', 'did', 'but', 'if', 'or', 'and',
|
|
18
|
-
'of', 'at', 'by', 'for', 'with', 'about', 'to', 'from', 'in', 'on',
|
|
19
|
-
'it', 'i', 'me', 'my', 'you', 'your', 'he', 'she', 'we', 'they', 'them',
|
|
20
|
-
'this', 'that', 'these', 'those', 'what', 'which', 'who', 'whom',
|
|
21
|
-
'not', 'will', 'just', 'so', 'up', 'out', 'no', 'yes', 'all', 'any'
|
|
22
|
-
]);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Helper to process posts and populate a word count map.
|
|
27
|
-
* @param {object} socialPostInsights - Map of { [postId]: postData }
|
|
28
|
-
* @param {object} targetCountMap - The object to store word counts.
|
|
29
|
-
*/
|
|
30
|
-
_countWords(socialPostInsights, targetCountMap) {
|
|
31
|
-
if (!socialPostInsights) return;
|
|
32
|
-
const posts = Object.values(socialPostInsights);
|
|
33
|
-
|
|
34
|
-
for (const post of posts) {
|
|
35
|
-
const text = post.fullText?.toLowerCase() || '';
|
|
36
|
-
if (!text) continue;
|
|
37
|
-
|
|
38
|
-
// Get all words 3+ chars long
|
|
39
|
-
const words = text.match(/\b[a-z]{3,}\b/g) || [];
|
|
40
|
-
|
|
41
|
-
for (const word of words) {
|
|
42
|
-
if (!this.stopWords.has(word)) {
|
|
43
|
-
targetCountMap[word] = (targetCountMap[word] || 0) + 1;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* @param {object} todaySocialPostInsights - Map of { [postId]: postData } for today.
|
|
51
|
-
* @param {object} yesterdaySocialPostInsights - Map of { [postId]: postData } for yesterday.
|
|
52
|
-
*/
|
|
53
|
-
async process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights, todaySocialPostInsights, yesterdaySocialPostInsights) {
|
|
54
|
-
|
|
55
|
-
if (this.processed) {
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
this.processed = true;
|
|
59
|
-
|
|
60
|
-
// Process today's posts
|
|
61
|
-
this._countWords(todaySocialPostInsights, this.todayWordCounts);
|
|
62
|
-
|
|
63
|
-
// Process yesterday's posts for trend calculation
|
|
64
|
-
this._countWords(yesterdaySocialPostInsights, this.yesterdayWordCounts);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async getResult() {
|
|
68
|
-
if (Object.keys(this.todayWordCounts).length === 0) {
|
|
69
|
-
return {};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const totalTodayWords = Object.values(this.todayWordCounts).reduce((a, b) => a + b, 0);
|
|
73
|
-
|
|
74
|
-
const topWords = Object.entries(this.todayWordCounts)
|
|
75
|
-
.sort(([, countA], [, countB]) => countB - countA)
|
|
76
|
-
.slice(0, 50); // Return the Top 50
|
|
77
|
-
|
|
78
|
-
const result = topWords.map(([term, count]) => {
|
|
79
|
-
const yesterdayCount = this.yesterdayWordCounts[term] || 0;
|
|
80
|
-
const trend = count - yesterdayCount;
|
|
81
|
-
const percent = (count / totalTodayWords) * 100;
|
|
82
|
-
|
|
83
|
-
return {
|
|
84
|
-
term,
|
|
85
|
-
count,
|
|
86
|
-
percent: parseFloat(percent.toFixed(2)),
|
|
87
|
-
trend
|
|
88
|
-
};
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
// Returns an array like:
|
|
92
|
-
// [ { term: "btc", count: 150, percent: 5.2, trend: 25 }, ... ]
|
|
93
|
-
return { top_words: result };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
reset() {
|
|
97
|
-
this.todayWordCounts = {};
|
|
98
|
-
this.yesterdayWordCounts = {};
|
|
99
|
-
this.processed = false;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Finds the top mentioned words in all posts for the day,
|
|
3
|
+
* filtering out common stop words, and calculates the trend from yesterday.
|
|
4
|
+
* This calculation ONLY uses the social post insights context.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class SocialTopMentionedWords {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.todayWordCounts = {};
|
|
10
|
+
this.yesterdayWordCounts = {};
|
|
11
|
+
this.processed = false;
|
|
12
|
+
|
|
13
|
+
// A small, representative list of stop words.
|
|
14
|
+
// In a real implementation, this list would be much larger.
|
|
15
|
+
this.stopWords = new Set([
|
|
16
|
+
'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
|
17
|
+
'have', 'has', 'had', 'do', 'does', 'did', 'but', 'if', 'or', 'and',
|
|
18
|
+
'of', 'at', 'by', 'for', 'with', 'about', 'to', 'from', 'in', 'on',
|
|
19
|
+
'it', 'i', 'me', 'my', 'you', 'your', 'he', 'she', 'we', 'they', 'them',
|
|
20
|
+
'this', 'that', 'these', 'those', 'what', 'which', 'who', 'whom',
|
|
21
|
+
'not', 'will', 'just', 'so', 'up', 'out', 'no', 'yes', 'all', 'any'
|
|
22
|
+
]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Helper to process posts and populate a word count map.
|
|
27
|
+
* @param {object} socialPostInsights - Map of { [postId]: postData }
|
|
28
|
+
* @param {object} targetCountMap - The object to store word counts.
|
|
29
|
+
*/
|
|
30
|
+
_countWords(socialPostInsights, targetCountMap) {
|
|
31
|
+
if (!socialPostInsights) return;
|
|
32
|
+
const posts = Object.values(socialPostInsights);
|
|
33
|
+
|
|
34
|
+
for (const post of posts) {
|
|
35
|
+
const text = post.fullText?.toLowerCase() || '';
|
|
36
|
+
if (!text) continue;
|
|
37
|
+
|
|
38
|
+
// Get all words 3+ chars long
|
|
39
|
+
const words = text.match(/\b[a-z]{3,}\b/g) || [];
|
|
40
|
+
|
|
41
|
+
for (const word of words) {
|
|
42
|
+
if (!this.stopWords.has(word)) {
|
|
43
|
+
targetCountMap[word] = (targetCountMap[word] || 0) + 1;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {object} todaySocialPostInsights - Map of { [postId]: postData } for today.
|
|
51
|
+
* @param {object} yesterdaySocialPostInsights - Map of { [postId]: postData } for yesterday.
|
|
52
|
+
*/
|
|
53
|
+
async process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights, todaySocialPostInsights, yesterdaySocialPostInsights) {
|
|
54
|
+
|
|
55
|
+
if (this.processed) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
this.processed = true;
|
|
59
|
+
|
|
60
|
+
// Process today's posts
|
|
61
|
+
this._countWords(todaySocialPostInsights, this.todayWordCounts);
|
|
62
|
+
|
|
63
|
+
// Process yesterday's posts for trend calculation
|
|
64
|
+
this._countWords(yesterdaySocialPostInsights, this.yesterdayWordCounts);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async getResult() {
|
|
68
|
+
if (Object.keys(this.todayWordCounts).length === 0) {
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const totalTodayWords = Object.values(this.todayWordCounts).reduce((a, b) => a + b, 0);
|
|
73
|
+
|
|
74
|
+
const topWords = Object.entries(this.todayWordCounts)
|
|
75
|
+
.sort(([, countA], [, countB]) => countB - countA)
|
|
76
|
+
.slice(0, 50); // Return the Top 50
|
|
77
|
+
|
|
78
|
+
const result = topWords.map(([term, count]) => {
|
|
79
|
+
const yesterdayCount = this.yesterdayWordCounts[term] || 0;
|
|
80
|
+
const trend = count - yesterdayCount;
|
|
81
|
+
const percent = (count / totalTodayWords) * 100;
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
term,
|
|
85
|
+
count,
|
|
86
|
+
percent: parseFloat(percent.toFixed(2)),
|
|
87
|
+
trend
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Returns an array like:
|
|
92
|
+
// [ { term: "btc", count: 150, percent: 5.2, trend: 25 }, ... ]
|
|
93
|
+
return { top_words: result };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
reset() {
|
|
97
|
+
this.todayWordCounts = {};
|
|
98
|
+
this.yesterdayWordCounts = {};
|
|
99
|
+
this.processed = false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
103
|
module.exports = SocialTopMentionedWords;
|