aiden-shared-calculations-unified 1.0.0
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/calculations/asset_metrics/asset_dollar_metrics.js +51 -0
- package/calculations/asset_metrics/asset_position_size.js +57 -0
- package/calculations/behavioural/drawdown_response.js +56 -0
- package/calculations/behavioural/gain_response.js +54 -0
- package/calculations/behavioural/paper_vs_diamond_hands.js +40 -0
- package/calculations/behavioural/position_count_pnl.js +50 -0
- package/calculations/behavioural/smart_money_flow.js +166 -0
- package/calculations/pnl/asset_pnl_status.js +47 -0
- package/calculations/pnl/average_daily_pnl_all_users.js +48 -0
- package/calculations/pnl/average_daily_pnl_per_sector.js +49 -0
- package/calculations/pnl/average_daily_pnl_per_stock.js +62 -0
- package/calculations/pnl/average_daily_position_pnl.js +40 -0
- package/calculations/pnl/pnl_distribution_per_stock.js +53 -0
- package/calculations/pnl/profitability_migration.js +58 -0
- package/calculations/pnl/profitability_ratio_per_stock.js +51 -0
- package/calculations/pnl/profitability_skew_per_stock.js +59 -0
- package/calculations/pnl/user_profitability_tracker.js +68 -0
- package/calculations/sanity/users_processed.js +27 -0
- package/calculations/sectors/diversification_pnl.js +59 -0
- package/calculations/sectors/sector_dollar_metrics.js +49 -0
- package/calculations/sectors/sector_rotation.js +68 -0
- package/calculations/sectors/total_long_per_sector.js +44 -0
- package/calculations/sectors/total_short_per_sector.js +44 -0
- package/calculations/sentiment/crowd_conviction_score.js +81 -0
- package/calculations/short_and_long_stats/long_position_per_stock.js +44 -0
- package/calculations/short_and_long_stats/sentiment_per_stock.js +50 -0
- package/calculations/short_and_long_stats/short_position_per_stock.js +43 -0
- package/calculations/short_and_long_stats/total_long_figures.js +33 -0
- package/calculations/short_and_long_stats/total_short_figures.js +34 -0
- package/calculations/speculators/distance_to_stop_loss_per_leverage.js +78 -0
- package/calculations/speculators/distance_to_tp_per_leverage.js +76 -0
- package/calculations/speculators/entry_distance_to_sl_per_leverage.js +78 -0
- package/calculations/speculators/entry_distance_to_tp_per_leverage.js +77 -0
- package/calculations/speculators/holding_duration_per_asset.js +55 -0
- package/calculations/speculators/leverage_per_asset.js +47 -0
- package/calculations/speculators/leverage_per_sector.js +45 -0
- package/calculations/speculators/risk_appetite_change.js +55 -0
- package/calculations/speculators/risk_reward_ratio_per_asset.js +60 -0
- package/calculations/speculators/speculator_asset_sentiment.js +81 -0
- package/calculations/speculators/speculator_danger_zone.js +58 -0
- package/calculations/speculators/stop_loss_distance_by_sector_short_long_breakdown.js +91 -0
- package/calculations/speculators/stop_loss_distance_by_ticker_short_long_breakdown.js +73 -0
- package/calculations/speculators/stop_loss_per_asset.js +55 -0
- package/calculations/speculators/take_profit_per_asset.js +55 -0
- package/calculations/speculators/tsl_effectiveness.js +55 -0
- package/calculations/speculators/tsl_per_asset.js +52 -0
- package/index.js +31 -0
- package/package.json +32 -0
- package/utils/firestore_utils.js +77 -0
- package/utils/sector_mapping_provider.js +75 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aggregates the total dollar amount invested in assets,
|
|
3
|
+
* broken down by profit or loss (from our sample).
|
|
4
|
+
*/
|
|
5
|
+
class AssetDollarMetrics {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.assets = {};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
_initAsset(ticker) {
|
|
11
|
+
if (!this.assets[ticker]) {
|
|
12
|
+
this.assets[ticker] = {
|
|
13
|
+
total_invested_sum: 0,
|
|
14
|
+
profit_invested_sum: 0,
|
|
15
|
+
loss_invested_sum: 0
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
process(portfolioData, userId, context) {
|
|
21
|
+
const { instrumentMappings } = context;
|
|
22
|
+
|
|
23
|
+
// FIX: Use the correct portfolio position properties
|
|
24
|
+
const positions = portfolioData.AggregatedPositions || portfolioData.PublicPositions;
|
|
25
|
+
if (!positions || !Array.isArray(positions)) return;
|
|
26
|
+
|
|
27
|
+
for (const position of positions) {
|
|
28
|
+
// FIX: Use the correct PascalCase InstrumentID
|
|
29
|
+
const ticker = instrumentMappings[position.InstrumentID];
|
|
30
|
+
if (!ticker) continue;
|
|
31
|
+
|
|
32
|
+
this._initAsset(ticker);
|
|
33
|
+
|
|
34
|
+
// FIX: Use the correct PascalCase InvestedAmount
|
|
35
|
+
this.assets[ticker].total_invested_sum += position.InvestedAmount;
|
|
36
|
+
|
|
37
|
+
// FIX: Use the correct PascalCase NetProfit
|
|
38
|
+
if (position.NetProfit > 0) {
|
|
39
|
+
this.assets[ticker].profit_invested_sum += position.InvestedAmount;
|
|
40
|
+
} else {
|
|
41
|
+
this.assets[ticker].loss_invested_sum += position.InvestedAmount;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
getResult() {
|
|
47
|
+
return this.assets;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = AssetDollarMetrics;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calculates the average position size for each asset.
|
|
3
|
+
*/
|
|
4
|
+
const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
|
|
5
|
+
|
|
6
|
+
class AssetPositionSize {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.assets = {};
|
|
9
|
+
this.mappings = null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
process(portfolioData, userId, context) {
|
|
13
|
+
const positions = portfolioData.AggregatedPositions || portfolioData.PublicPositions;
|
|
14
|
+
if (!positions || !Array.isArray(positions)) return;
|
|
15
|
+
|
|
16
|
+
for (const position of positions) {
|
|
17
|
+
const instrumentId = position.InstrumentID;
|
|
18
|
+
if (!instrumentId) continue;
|
|
19
|
+
|
|
20
|
+
if (!this.assets[instrumentId]) {
|
|
21
|
+
this.assets[instrumentId] = { position_count: 0, position_value_sum: 0 };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this.assets[instrumentId].position_count++;
|
|
25
|
+
this.assets[instrumentId].position_value_sum += (position.InvestedAmount || position.Amount || 0);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async getResult() {
|
|
30
|
+
if (!this.mappings) {
|
|
31
|
+
this.mappings = await loadInstrumentMappings();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const result = {};
|
|
35
|
+
for (const instrumentId in this.assets) {
|
|
36
|
+
const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId.toString();
|
|
37
|
+
const data = this.assets[instrumentId];
|
|
38
|
+
|
|
39
|
+
// REFACTOR: Perform final calculation and return in standardized format.
|
|
40
|
+
if (data.position_count > 0) {
|
|
41
|
+
result[ticker] = {
|
|
42
|
+
average_position_size: data.position_value_sum / data.position_count,
|
|
43
|
+
position_count: data.position_count // Also include count for context
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
reset() {
|
|
52
|
+
this.assets = {};
|
|
53
|
+
this.mappings = null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = AssetPositionSize;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analyzes user behavior after a position experiences a >10% drawdown.
|
|
3
|
+
*/
|
|
4
|
+
class DrawdownResponse {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.drawdown_events = {
|
|
7
|
+
held_position: 0,
|
|
8
|
+
closed_position: 0,
|
|
9
|
+
added_to_position: 0
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
process(todayPortfolio, yesterdayPortfolio, userId) {
|
|
14
|
+
if (!yesterdayPortfolio || !todayPortfolio) {
|
|
15
|
+
return; // Need both days for comparison
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// FIX: Get the correct positions arrays and ensure they are iterable
|
|
19
|
+
const yPositions = yesterdayPortfolio.AggregatedPositions || yesterdayPortfolio.PublicPositions;
|
|
20
|
+
const tPositions = todayPortfolio.AggregatedPositions || todayPortfolio.PublicPositions;
|
|
21
|
+
|
|
22
|
+
if (!yPositions || !Array.isArray(yPositions) || !tPositions || !Array.isArray(tPositions)) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Create a map of today's positions for efficient lookup
|
|
27
|
+
const todayPositions = new Map(tPositions.map(p => [p.PositionID, p]));
|
|
28
|
+
|
|
29
|
+
for (const yPos of yPositions) {
|
|
30
|
+
const drawdownPercent = yPos.InvestedAmount > 0 ? yPos.ProfitAndLoss / yPos.InvestedAmount : 0;
|
|
31
|
+
|
|
32
|
+
// Check if this position was in a >10% drawdown yesterday
|
|
33
|
+
if (drawdownPercent < -0.10) {
|
|
34
|
+
const todayPos = todayPositions.get(yPos.PositionID);
|
|
35
|
+
|
|
36
|
+
if (!todayPos) {
|
|
37
|
+
// Position was closed
|
|
38
|
+
this.drawdown_events.closed_position++;
|
|
39
|
+
} else if (todayPos.InvestedAmount > yPos.InvestedAmount) {
|
|
40
|
+
// User added money to the losing position
|
|
41
|
+
this.drawdown_events.added_to_position++;
|
|
42
|
+
} else {
|
|
43
|
+
// Position was held (or reduced, but not added to)
|
|
44
|
+
this.drawdown_events.held_position++;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getResult() {
|
|
51
|
+
// Return final calculated values
|
|
52
|
+
return this.drawdown_events;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = DrawdownResponse;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analyzes user behavior after a position experiences a >10% gain.
|
|
3
|
+
*/
|
|
4
|
+
class GainResponse {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.gain_events = {
|
|
7
|
+
held_position: 0,
|
|
8
|
+
closed_position: 0,
|
|
9
|
+
reduced_position: 0 // e.g., took partial profit
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
process(todayPortfolio, yesterdayPortfolio, userId) {
|
|
14
|
+
if (!yesterdayPortfolio || !todayPortfolio) {
|
|
15
|
+
return; // Need both days for comparison
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// FIX: Get the correct positions arrays and ensure they are iterable
|
|
19
|
+
const yPositions = yesterdayPortfolio.AggregatedPositions || yesterdayPortfolio.PublicPositions;
|
|
20
|
+
const tPositions = todayPortfolio.AggregatedPositions || todayPortfolio.PublicPositions;
|
|
21
|
+
|
|
22
|
+
if (!yPositions || !Array.isArray(yPositions) || !tPositions || !Array.isArray(tPositions)) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const todayPositions = new Map(tPositions.map(p => [p.PositionID, p]));
|
|
27
|
+
|
|
28
|
+
for (const yPos of yPositions) {
|
|
29
|
+
const gainPercent = yPos.InvestedAmount > 0 ? yPos.ProfitAndLoss / yPos.InvestedAmount : 0;
|
|
30
|
+
|
|
31
|
+
// Check if this position was in a >10% gain yesterday
|
|
32
|
+
if (gainPercent > 0.10) {
|
|
33
|
+
const todayPos = todayPositions.get(yPos.PositionID);
|
|
34
|
+
|
|
35
|
+
if (!todayPos) {
|
|
36
|
+
// Position was closed (took full profit)
|
|
37
|
+
this.gain_events.closed_position++;
|
|
38
|
+
} else if (todayPos.InvestedAmount < yPos.InvestedAmount) {
|
|
39
|
+
// User reduced the position (took partial profit)
|
|
40
|
+
this.gain_events.reduced_position++;
|
|
41
|
+
} else {
|
|
42
|
+
// Position was held (or added to)
|
|
43
|
+
this.gain_events.held_position++;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getResult() {
|
|
50
|
+
return this.gain_events;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = GainResponse;
|
|
@@ -0,0 +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
|
+
|
|
40
|
+
module.exports = PaperVsDiamondHands;
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
process(todayPortfolio, yesterdayPortfolio, userId) {
|
|
18
|
+
if (!todayPortfolio || !yesterdayPortfolio) {
|
|
19
|
+
return; // Need P/L and today's data
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// FIX: Get the correct positions array
|
|
23
|
+
const positions = todayPortfolio.AggregatedPositions || todayPortfolio.PublicPositions;
|
|
24
|
+
|
|
25
|
+
// FIX: Add check to ensure positions is an iterable array
|
|
26
|
+
if (!positions || !Array.isArray(positions)) {
|
|
27
|
+
return; // Skip users with no positions array
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const positionCount = positions.length;
|
|
31
|
+
if (positionCount === 0) {
|
|
32
|
+
return; // Skip users with no positions
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const dailyPnl = todayPortfolio.PortfolioValue - yesterdayPortfolio.PortfolioValue;
|
|
36
|
+
|
|
37
|
+
this._initBucket(positionCount);
|
|
38
|
+
this.pnl_by_position_count[positionCount].pnl_sum += dailyPnl;
|
|
39
|
+
this.pnl_by_position_count[positionCount].count++;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
getResult() {
|
|
43
|
+
// Return the aggregated object.
|
|
44
|
+
// Frontend will iterate keys, calculate avg (pnl_sum/count),
|
|
45
|
+
// and plot { x: positionCount, y: avg_pnl }
|
|
46
|
+
return this.pnl_by_position_count;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = PositionCountPnl;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tracks the investment flow of "smart money".
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { Firestore } = require('@google-cloud/firestore');
|
|
6
|
+
const firestore = new Firestore();
|
|
7
|
+
// CORRECTED PATH: ../utils/ instead of ../../utils/
|
|
8
|
+
const { getInstrumentSectorMap } = require('../../utils/sector_mapping_provider');
|
|
9
|
+
|
|
10
|
+
class SmartMoneyFlow {
|
|
11
|
+
// ... (rest of the code is unchanged) ...
|
|
12
|
+
constructor() {
|
|
13
|
+
this.smartMoneyUsers = new Set();
|
|
14
|
+
this.sectorFlow = {};
|
|
15
|
+
this.sectorMap = null; // Cache for the sector map
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async process(todayPortfolio, yesterdayPortfolio, userId) {
|
|
19
|
+
// Load smart money users only if the set is empty
|
|
20
|
+
if (this.smartMoneyUsers.size === 0) {
|
|
21
|
+
await this.identifySmartMoney();
|
|
22
|
+
// Log after identification attempt
|
|
23
|
+
console.log(`Identified ${this.smartMoneyUsers.size} smart money users.`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Load sector map if not already loaded
|
|
27
|
+
if (!this.sectorMap) {
|
|
28
|
+
try {
|
|
29
|
+
this.sectorMap = await getInstrumentSectorMap();
|
|
30
|
+
if (!this.sectorMap || Object.keys(this.sectorMap).length === 0) {
|
|
31
|
+
console.warn('Sector map loaded but is empty.');
|
|
32
|
+
} else {
|
|
33
|
+
// console.log('Sector map loaded successfully.'); // Optional: remove if too verbose
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('Failed to load sector map:', error);
|
|
37
|
+
return; // Stop processing if sector map fails to load
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check if the current user is considered "smart money"
|
|
42
|
+
if (this.smartMoneyUsers.has(userId) && todayPortfolio && yesterdayPortfolio) {
|
|
43
|
+
// Ensure sectorMap is available before proceeding
|
|
44
|
+
if (!this.sectorMap) {
|
|
45
|
+
console.warn(`Skipping user ${userId}: Sector map not available.`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const todaySectorInvestment = this.calculateSectorInvestment(todayPortfolio);
|
|
49
|
+
const yesterdaySectorInvestment = this.calculateSectorInvestment(yesterdayPortfolio);
|
|
50
|
+
|
|
51
|
+
// Calculate change in investment per sector
|
|
52
|
+
const allSectors = new Set([...Object.keys(todaySectorInvestment), ...Object.keys(yesterdaySectorInvestment)]);
|
|
53
|
+
for (const sector of allSectors) {
|
|
54
|
+
const todayAmount = todaySectorInvestment[sector] || 0;
|
|
55
|
+
const yesterdayAmount = yesterdaySectorInvestment[sector] || 0;
|
|
56
|
+
const change = todayAmount - yesterdayAmount;
|
|
57
|
+
|
|
58
|
+
// Only record if there is a change
|
|
59
|
+
if (change !== 0) {
|
|
60
|
+
if (!this.sectorFlow[sector]) {
|
|
61
|
+
this.sectorFlow[sector] = 0;
|
|
62
|
+
}
|
|
63
|
+
this.sectorFlow[sector] += change;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
// Optional: Log why processing is skipped for a user
|
|
68
|
+
// if (!this.smartMoneyUsers.has(userId)) console.log(`User ${userId} is not smart money.`);
|
|
69
|
+
// if (!todayPortfolio) console.log(`User ${userId}: Missing today's portfolio.`);
|
|
70
|
+
// if (!yesterdayPortfolio) console.log(`User ${userId}: Missing yesterday's portfolio.`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async identifySmartMoney() {
|
|
75
|
+
console.log("Attempting to identify smart money users...");
|
|
76
|
+
try {
|
|
77
|
+
// Fetching sharded profitability data
|
|
78
|
+
const shardPromises = [];
|
|
79
|
+
const NUM_SHARDS = 50; // Ensure this matches the value used in user_profitability_tracker
|
|
80
|
+
for (let i = 0; i < NUM_SHARDS; i++) {
|
|
81
|
+
// Corrected document path for shards
|
|
82
|
+
const docRef = firestore.collection('historical_insights').doc(`user_profitability_shard_${i}`);
|
|
83
|
+
shardPromises.push(docRef.get());
|
|
84
|
+
}
|
|
85
|
+
const shardSnapshots = await Promise.all(shardPromises);
|
|
86
|
+
|
|
87
|
+
let profitableUsersFound = 0;
|
|
88
|
+
shardSnapshots.forEach((snap, index) => {
|
|
89
|
+
if (snap.exists) {
|
|
90
|
+
const shardData = snap.data().profits || {}; // Assuming data structure { profits: { userId: [...] } }
|
|
91
|
+
for (const userId in shardData) {
|
|
92
|
+
const history = shardData[userId];
|
|
93
|
+
// Check if history is an array and has enough entries
|
|
94
|
+
if (Array.isArray(history) && history.length >= 5) {
|
|
95
|
+
// "Smart money" definition: profitable for at least 5 of the last 7 days
|
|
96
|
+
// Filter based on the 'pnl' property of each history entry
|
|
97
|
+
const profitableDays = history.slice(-7).filter(d => d && typeof d.pnl === 'number' && d.pnl > 0).length;
|
|
98
|
+
if (profitableDays >= 5) {
|
|
99
|
+
this.smartMoneyUsers.add(userId);
|
|
100
|
+
profitableUsersFound++;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
console.warn(`Profitability shard user_profitability_shard_${index} does not exist.`);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
console.log(`Found ${profitableUsersFound} potentially smart money users across all shards.`);
|
|
109
|
+
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error('Error identifying smart money users:', error);
|
|
112
|
+
// Decide how to handle error: maybe retry, or proceed without smart money data
|
|
113
|
+
this.smartMoneyUsers.clear(); // Ensure set is empty on error
|
|
114
|
+
}
|
|
115
|
+
// Final check after attempt
|
|
116
|
+
if (this.smartMoneyUsers.size === 0) {
|
|
117
|
+
console.warn("No smart money users identified. Smart money flow calculation might be empty.");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
calculateSectorInvestment(portfolio) {
|
|
123
|
+
const sectorInvestment = {};
|
|
124
|
+
// Ensure sectorMap is loaded
|
|
125
|
+
if (!this.sectorMap) {
|
|
126
|
+
console.warn("Cannot calculate sector investment: Sector map not loaded.");
|
|
127
|
+
return sectorInvestment; // Return empty object
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Use AggregatedPositions as it contains 'Invested' amount
|
|
131
|
+
if (portfolio && portfolio.AggregatedPositions && Array.isArray(portfolio.AggregatedPositions)) {
|
|
132
|
+
for (const pos of portfolio.AggregatedPositions) {
|
|
133
|
+
if (pos && typeof pos.InstrumentID !== 'undefined' && typeof pos.Invested === 'number') {
|
|
134
|
+
const sector = this.sectorMap[pos.InstrumentID] || 'N/A';
|
|
135
|
+
if (!sectorInvestment[sector]) {
|
|
136
|
+
sectorInvestment[sector] = 0;
|
|
137
|
+
}
|
|
138
|
+
sectorInvestment[sector] += pos.Invested;
|
|
139
|
+
} else {
|
|
140
|
+
// Log potentially malformed position data
|
|
141
|
+
// console.warn('Skipping position due to missing InstrumentID or Invested amount:', pos);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
// Log if AggregatedPositions are missing or not an array
|
|
146
|
+
// console.warn('AggregatedPositions missing or invalid in portfolio for sector investment calculation.');
|
|
147
|
+
}
|
|
148
|
+
return sectorInvestment;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
getResult() {
|
|
153
|
+
// Return only sectors with non-zero flow if desired, or the full object
|
|
154
|
+
const filteredFlow = {};
|
|
155
|
+
for (const sector in this.sectorFlow) {
|
|
156
|
+
if (this.sectorFlow[sector] !== 0) {
|
|
157
|
+
filteredFlow[sector] = this.sectorFlow[sector];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// console.log("Final Smart Money Flow:", filteredFlow); // Optional: Log final result
|
|
161
|
+
return { smart_money_flow: filteredFlow };
|
|
162
|
+
// Or return the full object: return { smart_money_flow: this.sectorFlow };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = SmartMoneyFlow;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Counts the number of users in profit vs. loss for each asset (from our sample).
|
|
3
|
+
* This data is used for extrapolation with the ground truth owner count.
|
|
4
|
+
*/
|
|
5
|
+
class AssetPnlStatus {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.assets = {};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
_initAsset(ticker) {
|
|
11
|
+
if (!this.assets[ticker]) {
|
|
12
|
+
this.assets[ticker] = { profit_count: 0, loss_count: 0 };
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
process(portfolioData, userId, context) {
|
|
17
|
+
const { instrumentMappings } = context;
|
|
18
|
+
const processedTickers = new Set(); // Ensure one user is only counted once per ticker
|
|
19
|
+
|
|
20
|
+
// FIX: Use the correct portfolio position properties
|
|
21
|
+
const positions = portfolioData.AggregatedPositions || portfolioData.PublicPositions;
|
|
22
|
+
if (!positions || !Array.isArray(positions)) return;
|
|
23
|
+
|
|
24
|
+
for (const position of positions) {
|
|
25
|
+
// FIX: Use the correct PascalCase InstrumentID
|
|
26
|
+
const ticker = instrumentMappings[position.InstrumentID];
|
|
27
|
+
if (!ticker || processedTickers.has(ticker)) continue;
|
|
28
|
+
|
|
29
|
+
this._initAsset(ticker);
|
|
30
|
+
|
|
31
|
+
// FIX: Use the correct PascalCase NetProfit
|
|
32
|
+
if (position.NetProfit > 0) {
|
|
33
|
+
this.assets[ticker].profit_count++;
|
|
34
|
+
} else {
|
|
35
|
+
this.assets[ticker].loss_count++;
|
|
36
|
+
}
|
|
37
|
+
processedTickers.add(ticker);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getResult() {
|
|
42
|
+
// Returns raw counts for frontend extrapolation
|
|
43
|
+
return this.assets;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = AssetPnlStatus;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Calculates the average PnL (NetProfit %) on a per-user basis, and then averages those user PnLs.
|
|
3
|
+
* This metric answers: "What was the average daily PnL for the average user?"
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class AverageDailyPnlAllUsers {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.totalOfUserAveragePnls = 0;
|
|
9
|
+
this.userCount = 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
process(portfolioData, userId, context) {
|
|
13
|
+
if (!portfolioData || !portfolioData.AggregatedPositions || portfolioData.AggregatedPositions.length === 0) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let userTotalPnl = 0;
|
|
18
|
+
const positionCount = portfolioData.AggregatedPositions.length;
|
|
19
|
+
|
|
20
|
+
for (const position of portfolioData.AggregatedPositions) {
|
|
21
|
+
userTotalPnl += position.NetProfit;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const userAveragePnl = userTotalPnl / positionCount;
|
|
25
|
+
this.totalOfUserAveragePnls += userAveragePnl;
|
|
26
|
+
this.userCount++;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
getResult() {
|
|
30
|
+
if (this.userCount === 0) {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// REFACTOR: Perform the final calculation directly.
|
|
35
|
+
const average_daily_pnl = this.totalOfUserAveragePnls / this.userCount;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
average_daily_pnl: average_daily_pnl
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
reset() {
|
|
43
|
+
this.totalOfUserAveragePnls = 0;
|
|
44
|
+
this.userCount = 0;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = AverageDailyPnlAllUsers;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Calculates the average daily PnL (NetProfit %) for each sector.
|
|
3
|
+
* This is a simple average, not weighted by position size.
|
|
4
|
+
*/
|
|
5
|
+
const { getInstrumentSectorMap } = require('../../utils/sector_mapping_provider');
|
|
6
|
+
|
|
7
|
+
class AverageDailyPnlPerSector {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.positions = [];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
process(portfolioData, userId, context) {
|
|
13
|
+
if (portfolioData && portfolioData.AggregatedPositions) {
|
|
14
|
+
this.positions.push(...portfolioData.AggregatedPositions);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async getResult() {
|
|
19
|
+
if (this.positions.length === 0) return {};
|
|
20
|
+
|
|
21
|
+
const sectorPnlSum = {};
|
|
22
|
+
const positionCountBySector = {};
|
|
23
|
+
const sectorMap = await getInstrumentSectorMap();
|
|
24
|
+
|
|
25
|
+
for (const position of this.positions) {
|
|
26
|
+
const instrumentId = position.InstrumentID;
|
|
27
|
+
const sector = sectorMap[instrumentId] || 'N/A';
|
|
28
|
+
|
|
29
|
+
sectorPnlSum[sector] = (sectorPnlSum[sector] || 0) + position.NetProfit;
|
|
30
|
+
positionCountBySector[sector] = (positionCountBySector[sector] || 0) + 1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const result = {};
|
|
34
|
+
for (const sector in sectorPnlSum) {
|
|
35
|
+
// REFACTOR: Calculate the final average directly.
|
|
36
|
+
result[sector] = {
|
|
37
|
+
average_pnl: sectorPnlSum[sector] / positionCountBySector[sector]
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
reset() {
|
|
45
|
+
this.positions = [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = AverageDailyPnlPerSector;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Calculates the average daily PnL (NetProfit) for each individual stock.
|
|
3
|
+
* This metric answers: "What was the average PnL for a position in this specific stock?"
|
|
4
|
+
*/
|
|
5
|
+
const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
|
|
6
|
+
|
|
7
|
+
class AverageDailyPnlPerStock {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.pnlData = {};
|
|
10
|
+
this.mappings = null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
process(portfolioData, userId, context) {
|
|
14
|
+
const positions = portfolioData.AggregatedPositions || portfolioData.PublicPositions;
|
|
15
|
+
if (!positions) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
for (const position of positions) {
|
|
20
|
+
const instrumentId = position.InstrumentID;
|
|
21
|
+
const netProfit = position.NetProfit;
|
|
22
|
+
|
|
23
|
+
if (!this.pnlData[instrumentId]) {
|
|
24
|
+
this.pnlData[instrumentId] = {
|
|
25
|
+
pnl_sum: 0,
|
|
26
|
+
position_count: 0
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
this.pnlData[instrumentId].pnl_sum += netProfit;
|
|
31
|
+
this.pnlData[instrumentId].position_count++;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async getResult() {
|
|
36
|
+
if (!this.mappings) {
|
|
37
|
+
this.mappings = await loadInstrumentMappings();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const result = {};
|
|
41
|
+
for (const instrumentId in this.pnlData) {
|
|
42
|
+
const ticker = this.mappings.instrumentToTicker[instrumentId] || instrumentId.toString();
|
|
43
|
+
const data = this.pnlData[instrumentId];
|
|
44
|
+
|
|
45
|
+
// REFACTOR: Perform the final calculation directly.
|
|
46
|
+
if (data.position_count > 0) {
|
|
47
|
+
result[ticker] = {
|
|
48
|
+
average_daily_pnl: data.pnl_sum / data.position_count
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
reset() {
|
|
57
|
+
this.pnlData = {};
|
|
58
|
+
this.mappings = null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = AverageDailyPnlPerStock;
|