aiden-shared-calculations-unified 1.0.63 → 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
|
@@ -1,61 +1,126 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* @fileoverview Calculation (Pass 2) for daily bought vs. sold count.
|
|
3
|
+
*
|
|
4
|
+
* This metric tracks the total number of positions 'bought' (new)
|
|
5
|
+
* and 'sold' (closed) today, based on daily ownership change.
|
|
6
|
+
*
|
|
7
|
+
* This is different from 'daily_asset_activity' because it counts
|
|
8
|
+
* *positions*, not *unique users*.
|
|
5
9
|
*/
|
|
10
|
+
const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
|
|
6
11
|
|
|
7
12
|
class DailyBoughtVsSoldCount {
|
|
8
13
|
constructor() {
|
|
9
|
-
|
|
10
|
-
this.
|
|
11
|
-
this.
|
|
14
|
+
// We will store { [instrumentId]: { new: 0, closed: 0 } }
|
|
15
|
+
this.assetActivity = new Map();
|
|
16
|
+
this.mappings = null;
|
|
12
17
|
}
|
|
13
18
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Defines the output schema for this calculation.
|
|
21
|
+
* @returns {object} JSON Schema object
|
|
22
|
+
*/
|
|
23
|
+
static getSchema() {
|
|
24
|
+
const tickerSchema = {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"description": "Daily trade activity for a specific asset.",
|
|
27
|
+
"properties": {
|
|
28
|
+
"positions_bought": {
|
|
29
|
+
"type": "number",
|
|
30
|
+
"description": "Total new positions opened for this asset."
|
|
31
|
+
},
|
|
32
|
+
"positions_sold": {
|
|
33
|
+
"type": "number",
|
|
34
|
+
"description": "Total positions closed for this asset."
|
|
35
|
+
},
|
|
36
|
+
"net_change": {
|
|
37
|
+
"type": "number",
|
|
38
|
+
"description": "Net change in positions (bought - sold)."
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"required": ["positions_bought", "positions_sold", "net_change"]
|
|
42
|
+
};
|
|
18
43
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
44
|
+
return {
|
|
45
|
+
"type": "object",
|
|
46
|
+
"description": "Tracks new positions (bought) and closed positions (sold) per asset.",
|
|
47
|
+
"patternProperties": {
|
|
48
|
+
"^.*$": tickerSchema // Matches any string key (ticker)
|
|
49
|
+
},
|
|
50
|
+
"additionalProperties": tickerSchema
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_initAsset(instrumentId) {
|
|
55
|
+
if (!this.assetActivity.has(instrumentId)) {
|
|
56
|
+
this.assetActivity.set(instrumentId, {
|
|
57
|
+
new: 0,
|
|
58
|
+
closed: 0
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_getInstrumentIds(portfolio) {
|
|
64
|
+
// This MUST use PositionID to be accurate
|
|
65
|
+
const positions = portfolio?.AggregatedPositions || portfolio?.PublicPositions;
|
|
66
|
+
if (!positions || !Array.isArray(positions)) {
|
|
67
|
+
return new Map();
|
|
22
68
|
}
|
|
69
|
+
// Map<PositionID, InstrumentID>
|
|
70
|
+
return new Map(positions.map(p => [p.PositionID, p.InstrumentID]).filter(p => p[0] && p[1]));
|
|
71
|
+
}
|
|
23
72
|
|
|
24
|
-
|
|
73
|
+
process(todayPortfolio, yesterdayPortfolio) {
|
|
74
|
+
if (!todayPortfolio || !yesterdayPortfolio) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
25
77
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const todayTotal = instrument.total || 0;
|
|
29
|
-
const yesterdayTotal = yesterdayTotals.get(instrumentId) || 0;
|
|
78
|
+
const yPosMap = this._getInstrumentIds(yesterdayPortfolio); // Map<PosID, InstID>
|
|
79
|
+
const tPosMap = this._getInstrumentIds(todayPortfolio); // Map<PosID, InstID>
|
|
30
80
|
|
|
31
|
-
|
|
81
|
+
// Find new positions (in today but not yesterday)
|
|
82
|
+
for (const [tPosId, tInstId] of tPosMap.entries()) {
|
|
83
|
+
if (!yPosMap.has(tPosId)) {
|
|
84
|
+
this._initAsset(tInstId);
|
|
85
|
+
this.assetActivity.get(tInstId).new++;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
32
88
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
this.
|
|
89
|
+
// Find closed positions (in yesterday but not today)
|
|
90
|
+
for (const [yPosId, yInstId] of yPosMap.entries()) {
|
|
91
|
+
if (!tPosMap.has(yPosId)) {
|
|
92
|
+
this._initAsset(yInstId);
|
|
93
|
+
this.assetActivity.get(yInstId).closed++;
|
|
37
94
|
}
|
|
38
95
|
}
|
|
39
96
|
}
|
|
40
97
|
|
|
41
|
-
getResult() {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
console.warn('[DailyBoughtVsSoldCount] getResult called but process never ran (missing insights). Returning null for backfill.');
|
|
45
|
-
return null;
|
|
98
|
+
async getResult() {
|
|
99
|
+
if (!this.mappings) {
|
|
100
|
+
this.mappings = await loadInstrumentMappings();
|
|
46
101
|
}
|
|
47
|
-
// --- END ADD ---
|
|
48
102
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
103
|
+
const result = {};
|
|
104
|
+
for (const [instrumentId, data] of this.assetActivity.entries()) {
|
|
105
|
+
const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
|
|
106
|
+
|
|
107
|
+
const openCount = data.new;
|
|
108
|
+
const closeCount = data.closed;
|
|
109
|
+
|
|
110
|
+
if (openCount > 0 || closeCount > 0) {
|
|
111
|
+
result[ticker] = {
|
|
112
|
+
positions_bought: openCount,
|
|
113
|
+
positions_sold: closeCount,
|
|
114
|
+
net_change: openCount - closeCount
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
53
119
|
}
|
|
54
120
|
|
|
55
121
|
reset() {
|
|
56
|
-
this.
|
|
57
|
-
this.
|
|
58
|
-
this.processedDay = false;
|
|
122
|
+
this.assetActivity.clear();
|
|
123
|
+
this.mappings = null;
|
|
59
124
|
}
|
|
60
125
|
}
|
|
61
126
|
|
|
@@ -1,60 +1,123 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview
|
|
3
|
-
*
|
|
2
|
+
* @fileoverview Calculation (Pass 2) for daily ownership delta.
|
|
3
|
+
*
|
|
4
|
+
* This metric calculates the daily change in the total number of *owners*
|
|
5
|
+
* (unique users) for each instrument.
|
|
6
|
+
*
|
|
7
|
+
* This measures the broadening or narrowing of an asset's holder base.
|
|
4
8
|
*/
|
|
5
9
|
const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
|
|
6
10
|
|
|
7
11
|
class DailyOwnershipDelta {
|
|
8
12
|
constructor() {
|
|
9
|
-
|
|
13
|
+
// We will store { [instrumentId]: { owners_yesterday: Set(), owners_today: Set() } }
|
|
14
|
+
this.assetOwnership = new Map();
|
|
10
15
|
this.mappings = null;
|
|
11
16
|
}
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Defines the output schema for this calculation.
|
|
20
|
+
* @returns {object} JSON Schema object
|
|
21
|
+
*/
|
|
22
|
+
static getSchema() {
|
|
23
|
+
const tickerSchema = {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"description": "Daily change in unique owners for a specific asset.",
|
|
26
|
+
"properties": {
|
|
27
|
+
"owners_today": {
|
|
28
|
+
"type": "number",
|
|
29
|
+
"description": "Total unique users holding this asset today."
|
|
30
|
+
},
|
|
31
|
+
"owners_yesterday": {
|
|
32
|
+
"type": "number",
|
|
33
|
+
"description": "Total unique users holding this asset yesterday."
|
|
34
|
+
},
|
|
35
|
+
"owner_delta": {
|
|
36
|
+
"type": "number",
|
|
37
|
+
"description": "The net change in unique owners (today - yesterday)."
|
|
38
|
+
},
|
|
39
|
+
"owner_delta_percent": {
|
|
40
|
+
"type": ["number", "null"],
|
|
41
|
+
"description": "Percentage change in unique owners. Null if yesterday had 0 owners."
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"required": ["owners_today", "owners_yesterday", "owner_delta", "owner_delta_percent"]
|
|
45
|
+
};
|
|
20
46
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
47
|
+
return {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"description": "Calculates the daily change in the total number of unique owners for each asset.",
|
|
50
|
+
"patternProperties": {
|
|
51
|
+
"^.*$": tickerSchema // Matches any string key (ticker)
|
|
52
|
+
},
|
|
53
|
+
"additionalProperties": tickerSchema
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
_initAsset(instrumentId) {
|
|
58
|
+
if (!this.assetOwnership.has(instrumentId)) {
|
|
59
|
+
this.assetOwnership.set(instrumentId, {
|
|
60
|
+
owners_yesterday: new Set(),
|
|
61
|
+
owners_today: new Set()
|
|
62
|
+
});
|
|
24
63
|
}
|
|
64
|
+
}
|
|
25
65
|
|
|
26
|
-
|
|
27
|
-
|
|
66
|
+
_getInstrumentIds(portfolio) {
|
|
67
|
+
const positions = portfolio?.AggregatedPositions || portfolio?.PublicPositions;
|
|
68
|
+
if (!positions || !Array.isArray(positions)) {
|
|
69
|
+
return new Set();
|
|
28
70
|
}
|
|
71
|
+
return new Set(positions.map(p => p.InstrumentID).filter(Boolean));
|
|
72
|
+
}
|
|
29
73
|
|
|
30
|
-
|
|
74
|
+
process(todayPortfolio, yesterdayPortfolio, userId) {
|
|
75
|
+
if (!todayPortfolio || !yesterdayPortfolio) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
31
78
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const todayTotal = instrument.total || 0;
|
|
35
|
-
const yesterdayTotal = yesterdayTotals.get(instrumentId) || 0; // Default to 0 if not found yesterday
|
|
79
|
+
const yIds = this._getInstrumentIds(yesterdayPortfolio);
|
|
80
|
+
const tIds = this._getInstrumentIds(todayPortfolio);
|
|
36
81
|
|
|
37
|
-
|
|
38
|
-
|
|
82
|
+
// Add user to yesterday's owner sets
|
|
83
|
+
for (const yId of yIds) {
|
|
84
|
+
this._initAsset(yId);
|
|
85
|
+
this.assetOwnership.get(yId).owners_yesterday.add(userId);
|
|
86
|
+
}
|
|
39
87
|
|
|
40
|
-
|
|
88
|
+
// Add user to today's owner sets
|
|
89
|
+
for (const tId of tIds) {
|
|
90
|
+
this._initAsset(tId);
|
|
91
|
+
this.assetOwnership.get(tId).owners_today.add(userId);
|
|
41
92
|
}
|
|
42
93
|
}
|
|
43
94
|
|
|
44
|
-
getResult() {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
console.warn('[DailyOwnershipDelta] getResult called but process never ran (missing insights). Returning null for backfill.');
|
|
48
|
-
return null;
|
|
95
|
+
async getResult() {
|
|
96
|
+
if (!this.mappings) {
|
|
97
|
+
this.mappings = await loadInstrumentMappings();
|
|
49
98
|
}
|
|
50
|
-
// --- END ADD ---
|
|
51
99
|
|
|
52
|
-
|
|
100
|
+
const result = {};
|
|
101
|
+
for (const [instrumentId, data] of this.assetOwnership.entries()) {
|
|
102
|
+
const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
|
|
103
|
+
|
|
104
|
+
const yOwners = data.owners_yesterday.size;
|
|
105
|
+
const tOwners = data.owners_today.size;
|
|
106
|
+
|
|
107
|
+
if (yOwners > 0 || tOwners > 0) {
|
|
108
|
+
result[ticker] = {
|
|
109
|
+
owners_today: tOwners,
|
|
110
|
+
owners_yesterday: yOwners,
|
|
111
|
+
owner_delta: tOwners - yOwners,
|
|
112
|
+
owner_delta_percent: (yOwners > 0) ? ((tOwners - yOwners) / yOwners) * 100 : null
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
53
117
|
}
|
|
54
118
|
|
|
55
119
|
reset() {
|
|
56
|
-
this.
|
|
57
|
-
this.processedDay = false; // Reset the flag for the next day
|
|
120
|
+
this.assetOwnership.clear();
|
|
58
121
|
this.mappings = null;
|
|
59
122
|
}
|
|
60
123
|
}
|
|
@@ -1,140 +1,108 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview
|
|
3
|
-
* following a net deposit signal.
|
|
2
|
+
* @fileoverview Calculation (Pass 3) for capital deployment strategy.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* This metric answers: "Following a net deposit event, does the crowd
|
|
5
|
+
* tend to deploy new capital into new assets or add to existing ones?"
|
|
6
|
+
*
|
|
7
|
+
* It *depends* on 'crowd-cash-flow-proxy', 'new_allocation_percentage',
|
|
8
|
+
* and 'reallocation_increase_percentage'.
|
|
8
9
|
*/
|
|
9
|
-
|
|
10
|
-
const { FieldValue } = require('@google-cloud/firestore');
|
|
11
|
-
|
|
12
10
|
class CapitalDeploymentStrategy {
|
|
11
|
+
constructor() {
|
|
12
|
+
// This class only aggregates results from other calculations.
|
|
13
|
+
// No per-user processing is needed.
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Defines the output schema for this calculation.
|
|
18
|
+
* @returns {object} JSON Schema object
|
|
19
|
+
*/
|
|
20
|
+
static getSchema() {
|
|
21
|
+
return {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"description": "Analyzes if the crowd deploys new capital into new or existing assets, triggered by a net deposit event.",
|
|
24
|
+
"properties": {
|
|
25
|
+
"is_net_deposit_day": {
|
|
26
|
+
"type": "boolean",
|
|
27
|
+
"description": "True if today was a net deposit day for the crowd."
|
|
28
|
+
},
|
|
29
|
+
"net_cash_flow_proxy": {
|
|
30
|
+
"type": "number",
|
|
31
|
+
"description": "The estimated net cash flow (from 'crowd-cash-flow-proxy')."
|
|
32
|
+
},
|
|
33
|
+
"total_deployed_to_new_positions_pct": {
|
|
34
|
+
"type": "number",
|
|
35
|
+
"description": "The total % portfolio allocation deployed to new positions (from 'new_allocation_percentage')."
|
|
36
|
+
},
|
|
37
|
+
"total_deployed_to_existing_positions_pct": {
|
|
38
|
+
"type": "number",
|
|
39
|
+
"description": "The total % portfolio allocation added to existing positions (from 'reallocation_increase_percentage')."
|
|
40
|
+
},
|
|
41
|
+
"deployment_ratio_new_vs_existing": {
|
|
42
|
+
"type": ["number", "null"],
|
|
43
|
+
"description": "Ratio of (New / Existing) deployment. Null if no deployment to existing positions."
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"required": ["is_net_deposit_day", "net_cash_flow_proxy", "total_deployed_to_new_positions_pct", "total_deployed_to_existing_positions_pct", "deployment_ratio_new_vs_existing"]
|
|
47
|
+
};
|
|
48
|
+
}
|
|
13
49
|
|
|
14
50
|
/**
|
|
15
|
-
*
|
|
51
|
+
* Statically declare dependencies.
|
|
16
52
|
*/
|
|
17
53
|
static getDependencies() {
|
|
18
54
|
return [
|
|
19
|
-
'crowd-cash-flow-proxy',
|
|
20
|
-
'
|
|
21
|
-
'
|
|
55
|
+
'crowd-cash-flow-proxy',
|
|
56
|
+
'new_allocation_percentage',
|
|
57
|
+
'reallocation_increase_percentage'
|
|
22
58
|
];
|
|
23
59
|
}
|
|
24
|
-
|
|
25
|
-
constructor() {
|
|
26
|
-
this.lookbackDays = 7;
|
|
27
|
-
this.correlationWindow = 3; // How many days after a signal to link behavior
|
|
28
|
-
this.depositSignalThreshold = -0.005;
|
|
29
|
-
}
|
|
30
60
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
61
|
+
/**
|
|
62
|
+
* process() is a no-op. All logic is in getResult().
|
|
63
|
+
*/
|
|
64
|
+
process() {
|
|
65
|
+
// No-op
|
|
35
66
|
}
|
|
36
67
|
|
|
37
68
|
/**
|
|
38
|
-
*
|
|
39
|
-
* @param {string} dateStr The date to run the analysis for (e.g., "2025-10-31").
|
|
40
|
-
* @param {object} dependencies The shared dependencies (db, logger).
|
|
41
|
-
* @param {object} config The computation system configuration.
|
|
42
|
-
* @param {object} fetchedDependencies In-memory results from previous passes.
|
|
43
|
-
* @returns {Promise<object|null>} The analysis result or null.
|
|
69
|
+
* Aggregates results from dependencies.
|
|
44
70
|
*/
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const compsSub = config.computationsSubcollection || 'computations';
|
|
50
|
-
|
|
51
|
-
// 1. Find the most recent deposit signal (Still requires historical lookback)
|
|
52
|
-
let depositSignal = null;
|
|
53
|
-
let depositSignalDay = null;
|
|
54
|
-
let refsToGet = [];
|
|
71
|
+
getResult(fetchedDependencies) {
|
|
72
|
+
const cashFlowData = fetchedDependencies['crowd-cash-flow-proxy'];
|
|
73
|
+
const newAllocData = fetchedDependencies['new_allocation_percentage'];
|
|
74
|
+
const reallocData = fetchedDependencies['reallocation_increase_percentage'];
|
|
55
75
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
refsToGet.push({
|
|
59
|
-
date: checkDate,
|
|
60
|
-
key: `signal_${checkDate}`,
|
|
61
|
-
ref: db.collection(collection).doc(checkDate).collection(resultsSub).doc('capital_flow').collection(compsSub).doc('crowd-cash-flow-proxy')
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const signalSnapshots = await db.getAll(...refsToGet.map(r => r.ref));
|
|
66
|
-
const dataMap = new Map();
|
|
67
|
-
signalSnapshots.forEach((snap, idx) => {
|
|
68
|
-
if (snap.exists) dataMap.set(refsToGet[idx].key, snap.data());
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
for (let i = 1; i <= this.lookbackDays; i++) {
|
|
72
|
-
const checkDate = this._getDateStr(dateStr, i);
|
|
73
|
-
const flowData = dataMap.get(`signal_${checkDate}`);
|
|
74
|
-
if (flowData && flowData.cash_flow_effect_proxy < this.depositSignalThreshold) {
|
|
75
|
-
depositSignal = flowData;
|
|
76
|
-
depositSignalDay = checkDate;
|
|
77
|
-
break; // Found the most recent signal
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (!depositSignal) {
|
|
82
|
-
return {
|
|
83
|
-
status: 'no_deposit_signal_found',
|
|
84
|
-
lookback_days: this.lookbackDays
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// 2. Check if today is within the correlation window
|
|
89
|
-
const daysSinceSignal = (new Date(dateStr) - new Date(depositSignalDay)) / (1000 * 60 * 60 * 24);
|
|
90
|
-
|
|
91
|
-
if (daysSinceSignal <= 0 || daysSinceSignal > this.correlationWindow) {
|
|
76
|
+
if (!cashFlowData || !newAllocData || !reallocData) {
|
|
77
|
+
// Return a default "non-event" state if dependencies are missing
|
|
92
78
|
return {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
79
|
+
is_net_deposit_day: false,
|
|
80
|
+
net_cash_flow_proxy: 0,
|
|
81
|
+
total_deployed_to_new_positions_pct: 0,
|
|
82
|
+
total_deployed_to_existing_positions_pct: 0,
|
|
83
|
+
deployment_ratio_new_vs_existing: null
|
|
96
84
|
};
|
|
97
85
|
}
|
|
98
86
|
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
const reAllocData = fetchedDependencies['reallocation-increase-percentage'];
|
|
102
|
-
|
|
103
|
-
// 4. Handle missing dependencies
|
|
104
|
-
if (!newAllocData || !reAllocData) {
|
|
105
|
-
logger.log('WARN', `[CapitalDeploymentStrategy] Missing in-memory deployment data for ${dateStr}. Skipping.`);
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// 5. Calculate deployment bias
|
|
110
|
-
const newAlloc = newAllocData.average_new_allocation_percentage || 0;
|
|
111
|
-
const reAlloc = reAllocData.average_reallocation_increase_percentage || 0;
|
|
112
|
-
|
|
113
|
-
const totalDeployment = newAlloc + reAlloc;
|
|
114
|
-
let newAssetBias = 0;
|
|
115
|
-
let existingAssetBias = 0;
|
|
87
|
+
const netCashFlow = cashFlowData.net_cash_flow_proxy || 0;
|
|
88
|
+
const isNetDepositDay = netCashFlow > 0;
|
|
116
89
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
90
|
+
// We only report deployment stats IF it was a deposit day.
|
|
91
|
+
const newAllocSum = isNetDepositDay ? newAllocData.total_new_allocation_pct_sum : 0;
|
|
92
|
+
const reallocSum = isNetDepositDay ? reallocData.total_reallocation_increase_pct_sum : 0;
|
|
121
93
|
|
|
122
94
|
return {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
deployment_new_alloc_pct: newAlloc,
|
|
129
|
-
deployment_existing_alloc_pct: reAlloc,
|
|
130
|
-
total_deployment_pct: totalDeployment,
|
|
131
|
-
new_asset_bias: newAssetBias,
|
|
132
|
-
existing_asset_bias: existingAssetBias
|
|
95
|
+
is_net_deposit_day: isNetDepositDay,
|
|
96
|
+
net_cash_flow_proxy: netCashFlow,
|
|
97
|
+
total_deployed_to_new_positions_pct: newAllocSum,
|
|
98
|
+
total_deployed_to_existing_positions_pct: reallocSum,
|
|
99
|
+
deployment_ratio_new_vs_existing: (reallocSum > 0) ? (newAllocSum / reallocSum) : null
|
|
133
100
|
};
|
|
134
101
|
}
|
|
135
102
|
|
|
136
|
-
|
|
137
|
-
|
|
103
|
+
reset() {
|
|
104
|
+
// No state to reset
|
|
105
|
+
}
|
|
138
106
|
}
|
|
139
107
|
|
|
140
108
|
module.exports = CapitalDeploymentStrategy;
|