aiden-shared-calculations-unified 1.0.76 → 1.0.78

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.
Files changed (114) hide show
  1. package/calculations/{pnl/asset_pnl_status.js → core/asset-pnl-status.js} +20 -0
  2. package/calculations/{asset_metrics/asset_position_size.js → core/asset-position-size.js} +25 -1
  3. package/calculations/{pnl/average_daily_pnl_all_users.js → core/average-daily-pnl-all-users.js} +22 -0
  4. package/calculations/{pnl/average_daily_pnl_per_sector.js → core/average-daily-pnl-per-sector.js} +22 -0
  5. package/calculations/{pnl/average_daily_pnl_per_stock.js → core/average-daily-pnl-per-stock.js} +22 -0
  6. package/calculations/{pnl/average_daily_position_pnl.js → core/average-daily-position-pnl.js} +22 -0
  7. package/calculations/{behavioural/historical/holding_duration_per_asset.js → core/holding-duration-per-asset.js} +46 -5
  8. package/calculations/{meta/gem_instrument-price-momentum.js → core/instrument-price-momentum-20d.js} +17 -4
  9. package/calculations/{short_and_long_stats/long_position_per_stock.js → core/long-position-per-stock.js} +24 -0
  10. package/calculations/{behavioural/overall_holding_duration.js → core/overall-holding-duration.js} +27 -3
  11. package/calculations/{pnl/overall_profitability_ratio.js → core/overall-profitability-ratio.js} +24 -0
  12. package/calculations/{insights/daily_buy_sell_sentiment_count.js → core/platform-buy-sell-sentiment.js} +22 -0
  13. package/calculations/{insights/historical/daily_bought_vs_sold_count.js → core/platform-daily-bought-vs-sold-count.js} +24 -1
  14. package/calculations/{insights/historical/daily_ownership_delta.js → core/platform-daily-ownership-delta.js} +24 -1
  15. package/calculations/{insights/daily_ownership_per_sector.js → core/platform-ownership-per-sector.js} +22 -0
  16. package/calculations/{insights/daily_total_positions_held.js → core/platform-total-positions-held.js} +22 -0
  17. package/calculations/{pnl/pnl_distribution_per_stock.js → core/pnl-distribution-per-stock.js} +24 -0
  18. package/calculations/{pnl/profitability_ratio_per_sector,js → core/profitability-ratio-per-sector.js} +35 -5
  19. package/calculations/{pnl/profitability_ratio_per_stock.js → core/profitability-ratio-per-stock.js} +24 -0
  20. package/calculations/{pnl/profitability_skew_per_stock.js → core/profitability-skew-per-stock.js} +24 -0
  21. package/calculations/{pnl/profitable_and_unprofitable_status.js → core/profitable-and-unprofitable-status.js} +24 -0
  22. package/calculations/{short_and_long_stats/sentiment_per_stock.js → core/sentiment-per-stock.js} +20 -0
  23. package/calculations/{short_and_long_stats/short_position_per_stock.js → core/short-position-per-stock.js} +24 -0
  24. package/calculations/{socialPosts/social_activity_aggregation.js → core/social-activity-aggregation.js} +32 -8
  25. package/calculations/{socialPosts → core}/social-asset-posts-trend.js +30 -8
  26. package/calculations/{socialPosts/social_event_correlation.js → core/social-event-correlation.js} +24 -2
  27. package/calculations/{socialPosts/social_sentiment_aggregation.js → core/social-sentiment-aggregation.js} +23 -0
  28. package/calculations/{socialPosts → core}/social-top-mentioned-words.js +31 -8
  29. package/calculations/{socialPosts → core}/social-topic-interest-evolution.js +35 -11
  30. package/calculations/{socialPosts → core}/social-topic-sentiment-matrix.js +34 -10
  31. package/calculations/{socialPosts → core}/social-word-mentions-trend.js +36 -11
  32. package/calculations/{speculators/speculator_asset_sentiment.js → core/speculator-asset-sentiment.js} +20 -0
  33. package/calculations/{speculators/speculator_danger_zone.js → core/speculator-danger-zone.js} +22 -0
  34. package/calculations/{speculators/distance_to_stop_loss_per_leverage.js → core/speculator-distance-to-stop-loss-per-leverage.js} +24 -0
  35. package/calculations/{speculators/distance_to_tp_per_leverage.js → core/speculator-distance-to-tp-per-leverage.js} +24 -0
  36. package/calculations/{speculators/entry_distance_to_sl_per_leverage.js → core/speculator-entry-distance-to-sl-per-leverage.js} +24 -0
  37. package/calculations/{speculators/entry_distance_to_tp_per_leverage.js → core/speculator-entry-distance-to-tp-per-leverage.js} +24 -0
  38. package/calculations/{speculators/leverage_per_asset.js → core/speculator-leverage-per-asset.js} +20 -0
  39. package/calculations/{speculators/leverage_per_sector.js → core/speculator-leverage-per-sector.js} +22 -0
  40. package/calculations/{speculators/risk_reward_ratio_per_asset.js → core/speculator-risk-reward-ratio-per-asset.js} +20 -0
  41. package/calculations/{speculators/stop_loss_distance_by_sector_short_long_breakdown.js → core/speculator-stop-loss-distance-by-sector-short-long-breakdown.js} +22 -0
  42. package/calculations/{speculators/stop_loss_distance_by_ticker_short_long_breakdown.js → core/speculator-stop-loss-distance-by-ticker-short-long-breakdown.js} +24 -0
  43. package/calculations/{speculators/stop_loss_per_asset.js → core/speculator-stop-loss-per-asset.js} +24 -0
  44. package/calculations/{speculators/take_profit_per_asset.js → core/speculator-take-profit-per-asset.js} +24 -0
  45. package/calculations/{speculators/tsl_per_asset.js → core/speculator-tsl-per-asset.js} +24 -0
  46. package/calculations/{short_and_long_stats/total_long_figures.js → core/total-long-figures.js} +24 -0
  47. package/calculations/{sectors/total_long_per_sector.js → core/total-long-per-sector.js} +22 -0
  48. package/calculations/{short_and_long_stats/total_short_figures.js → core/total-short-figures.js} +24 -0
  49. package/calculations/{sectors/total_short_per_sector.js → core/total-short-per-sector.js} +22 -0
  50. package/calculations/{sanity/users_processed.js → core/users-processed.js} +22 -0
  51. package/calculations/gauss/cohort-capital-flow.js +217 -0
  52. package/calculations/gauss/cohort-definer.js +211 -0
  53. package/calculations/gauss/daily-dna-filter.js +130 -0
  54. package/calculations/gauss/gauss-divergence-signal.js +160 -0
  55. package/calculations/{meta/gem_cohort-momentum-state.js → gem/cohort-momentum-state.js} +22 -7
  56. package/calculations/{behavioural/historical/gem_cohort-skill-definition.js → gem/cohort-skill-definition.js} +18 -1
  57. package/calculations/{sentiment/gem_platform-conviction-divergence.js → gem/platform-conviction-divergence.js} +13 -0
  58. package/calculations/{meta/gem_quant-skill-alpha-signal.js → gem/quant-skill-alpha-signal.js} +25 -8
  59. package/calculations/{behavioural/historical/gem_skilled-cohort-flow.js → gem/skilled-cohort-flow.js} +18 -3
  60. package/calculations/{meta/gem_skilled-unskilled-divergence.js → gem/skilled-unskilled-divergence.js} +18 -4
  61. package/calculations/{behavioural/historical/gem_unskilled-cohort-flow.js → gem/unskilled-cohort-flow.js} +18 -3
  62. package/calculations/helix/helix-contrarian-signal.js +154 -0
  63. package/calculations/helix/herd-consensus-score.js +152 -0
  64. package/calculations/helix/winner-loser-flow.js +207 -0
  65. package/calculations/{behavioural/historical → legacy}/asset_crowd_flow.js +1 -1
  66. package/calculations/{sentiment/historical → legacy}/crowd_conviction_score.js +1 -1
  67. package/calculations/{activity/historical → legacy}/daily_asset_activity.js +1 -1
  68. package/calculations/{behavioural/historical → legacy}/dumb-cohort-flow.js +1 -1
  69. package/calculations/{behavioural/historical → legacy}/in_loss_asset_crowd_flow.js +1 -1
  70. package/calculations/{behavioural/historical → legacy}/in_profit_asset_crowd_flow.js +1 -1
  71. package/calculations/{speculators/historical → legacy}/risk_appetite_change.js +3 -1
  72. package/calculations/{sectors/historical → legacy}/sector_rotation.js +1 -1
  73. package/calculations/{behavioural/historical → legacy}/smart-cohort-flow.js +1 -1
  74. package/calculations/{behavioural/historical → legacy}/smart_money_flow.js +1 -1
  75. package/calculations/{behavioural/historical → legacy}/user-investment-profile.js +2 -2
  76. package/calculations/pyro/risk-appetite-index.js +153 -0
  77. package/calculations/pyro/squeeze-potential.js +158 -0
  78. package/calculations/pyro/volatility-signal.js +133 -0
  79. package/package.json +1 -1
  80. package/calculations/socialPosts/gem_social_sentiment_aggregation.js +0 -146
  81. /package/calculations/{activity/historical → legacy}/activity_by_pnl_status.js +0 -0
  82. /package/calculations/{meta → legacy}/capital_deployment_strategy.js +0 -0
  83. /package/calculations/{meta → legacy}/capital_liquidation_performance.js +0 -0
  84. /package/calculations/{meta → legacy}/capital_vintage_performance.js +0 -0
  85. /package/calculations/{meta → legacy}/cash-flow-deployment.js +0 -0
  86. /package/calculations/{meta → legacy}/cash-flow-liquidation.js +0 -0
  87. /package/calculations/{capital_flow/historical → legacy}/crowd-cash-flow-proxy.js +0 -0
  88. /package/calculations/{meta → legacy}/crowd_sharpe_ratio_proxy.js +0 -0
  89. /package/calculations/{activity/historical → legacy}/daily_user_activity_tracker.js +0 -0
  90. /package/calculations/{capital_flow/historical → legacy}/deposit_withdrawal_percentage.js +0 -0
  91. /package/calculations/{sectors/historical → legacy}/diversification_pnl.js +0 -0
  92. /package/calculations/{behavioural/historical → legacy}/drawdown_response.js +0 -0
  93. /package/calculations/{behavioural/historical → legacy}/gain_response.js +0 -0
  94. /package/calculations/{behavioural/historical → legacy}/historical_performance_aggregator.js +0 -0
  95. /package/calculations/{meta → legacy}/negative_expectancy_cohort_flow.js +0 -0
  96. /package/calculations/{capital_flow/historical → legacy}/new_allocation_percentage.js +0 -0
  97. /package/calculations/{behavioural/historical → legacy}/paper_vs_diamond_hands.js +0 -0
  98. /package/calculations/{behavioural/historical → legacy}/position_count_pnl.js +0 -0
  99. /package/calculations/{meta → legacy}/positive_expectancy_cohort_flow.js +0 -0
  100. /package/calculations/{meta → legacy}/profit_cohort_divergence.js +0 -0
  101. /package/calculations/{pnl/historical → legacy}/profitability_migration.js +0 -0
  102. /package/calculations/{capital_flow/historical → legacy}/reallocation_increase_percentage.js +0 -0
  103. /package/calculations/{meta → legacy}/shark_attack_signal.js +0 -0
  104. /package/calculations/{meta → legacy}/smart-dumb-divergence-index.js +0 -0
  105. /package/calculations/{meta → legacy}/smart_dumb_divergence_index_v2.js +0 -0
  106. /package/calculations/{meta → legacy}/social-predictive-regime-state.js +0 -0
  107. /package/calculations/{meta → legacy}/social-topic-driver-index.js +0 -0
  108. /package/calculations/{meta → legacy}/social-topic-predictive-potential.js +0 -0
  109. /package/calculations/{meta → legacy}/social_flow_correlation.js +0 -0
  110. /package/calculations/{activity/historical → legacy}/speculator_adjustment_activity.js +0 -0
  111. /package/calculations/{backtests → legacy}/strategy-performance.js +0 -0
  112. /package/calculations/{speculators/historical → legacy}/tsl_effectiveness.js +0 -0
  113. /package/calculations/{meta → legacy}/user_expectancy_score.js +0 -0
  114. /package/calculations/{pnl/historical → legacy}/user_profitability_tracker.js +0 -0
@@ -13,6 +13,28 @@ class TotalShortPerSector {
13
13
  this.mappings = null;
14
14
  }
15
15
 
16
+ // --- NEW ---
17
+ /**
18
+ * Statically defines all metadata for the manifest builder.
19
+ */
20
+ static getMetadata() {
21
+ return {
22
+ type: 'standard',
23
+ rootDataDependencies: ['portfolio'],
24
+ isHistorical: false,
25
+ userType: 'all',
26
+ category: 'core_sentiment'
27
+ };
28
+ }
29
+
30
+ // --- NEW ---
31
+ /**
32
+ * Statically declare dependencies.
33
+ */
34
+ static getDependencies() {
35
+ return [];
36
+ }
37
+
16
38
  /**
17
39
  * Defines the output schema for this calculation.
18
40
  * @returns {object} JSON Schema object
@@ -7,6 +7,28 @@ class UsersProcessed {
7
7
  this.userCount = 0;
8
8
  }
9
9
 
10
+ // --- NEW ---
11
+ /**
12
+ * Statically defines all metadata for the manifest builder.
13
+ */
14
+ static getMetadata() {
15
+ return {
16
+ type: 'standard',
17
+ rootDataDependencies: ['portfolio'], // Needs at least one doc to run
18
+ isHistorical: false,
19
+ userType: 'all',
20
+ category: 'core_sanity' // Based on Computation_documentation.md
21
+ };
22
+ }
23
+
24
+ // --- NEW ---
25
+ /**
26
+ * Statically declare dependencies.
27
+ */
28
+ static getDependencies() {
29
+ return [];
30
+ }
31
+
10
32
  /**
11
33
  * Defines the output schema for this calculation.
12
34
  * @returns {object} JSON Schema object
@@ -0,0 +1,217 @@
1
+ /**
2
+ * @fileoverview GAUSS Product Line (Pass 3)
3
+ *
4
+ * This 'standard' calculation streams all user portfolios, tags
5
+ * each user with their "Gauss" cohort (from Pass 2), and calculates
6
+ * the price-adjusted capital flow for each cohort, per asset.
7
+ *
8
+ * This is the primary input for the final Pass 4 signal.
9
+ */
10
+ const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
11
+
12
+
13
+ class CohortCapitalFlow {
14
+ constructor() {
15
+ // We will store: { [cohortName]: Map<instrumentId, { flow_data... }> }
16
+ this.cohortFlows = new Map();
17
+ // This will be a lookup map: { [userId]: "cohortName" }
18
+ this.cohortMap = new Map();
19
+ this.mappings = null;
20
+ this.dependenciesLoaded = false;
21
+ }
22
+
23
+ /**
24
+ * Defines the output schema for this calculation.
25
+ * @returns {object} JSON Schema object
26
+ */
27
+ static getSchema() {
28
+ const flowSchema = {
29
+ "type": "object",
30
+ "properties": {
31
+ "net_flow_percentage": { "type": "number" },
32
+ "net_flow_contribution": { "type": "number" }
33
+ },
34
+ "required": ["net_flow_percentage", "net_flow_contribution"]
35
+ };
36
+
37
+ const cohortSchema = {
38
+ "type": "object",
39
+ "description": "Price-adjusted capital flow for all assets for a specific cohort.",
40
+ "patternProperties": { "^.*$": flowSchema }, // Ticker
41
+ "additionalProperties": flowSchema
42
+ };
43
+
44
+ return {
45
+ "type": "object",
46
+ "description": "Contains a map of cohort names to their asset flow data.",
47
+ "patternProperties": {
48
+ "^.*$": cohortSchema // Cohort Name
49
+ },
50
+ "additionalProperties": cohortSchema
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Statically defines all metadata for the manifest builder.
56
+ */
57
+ static getMetadata() {
58
+ return {
59
+ type: 'standard',
60
+ rootDataDependencies: ['portfolio'],
61
+ isHistorical: true, // Needs T-1 portfolio for flow
62
+ userType: 'all',
63
+ category: 'gauss'
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Statically declare dependencies.
69
+ */
70
+ static getDependencies() {
71
+ return [
72
+ 'cohort-definer' // from gauss (Pass 2)
73
+ ];
74
+ }
75
+
76
+ _getPortfolioPositions(portfolio) {
77
+ // We MUST use AggregatedPositions for this to get 'Invested' (portfolio percentage)
78
+ return portfolio?.AggregatedPositions;
79
+ }
80
+
81
+ _initFlowData(cohortName, instrumentId) {
82
+ if (!this.cohortFlows.has(cohortName)) {
83
+ this.cohortFlows.set(cohortName, new Map());
84
+ }
85
+ if (!this.cohortFlows.get(cohortName).has(instrumentId)) {
86
+ this.cohortFlows.get(cohortName).set(instrumentId, {
87
+ total_invested_yesterday: 0,
88
+ total_invested_today: 0,
89
+ price_change_yesterday: 0, // This is a weighted sum
90
+ });
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Loads dependencies into memory on the first process() call.
96
+ */
97
+ _loadDependencies(fetchedDependencies) {
98
+ if (this.dependenciesLoaded) return;
99
+
100
+ const cohortData = fetchedDependencies['cohort-definer'];
101
+ if (cohortData) {
102
+ for (const [cohortName, userIds] of Object.entries(cohortData)) {
103
+ for (const userId of userIds) {
104
+ this.cohortMap.set(userId, cohortName);
105
+ }
106
+ }
107
+ }
108
+ this.dependenciesLoaded = true;
109
+ }
110
+
111
+ process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights, fetchedDependencies) {
112
+ this._loadDependencies(fetchedDependencies);
113
+
114
+ if (!this.mappings) {
115
+ this.mappings = context.mappings;
116
+ }
117
+
118
+ const cohortName = this.cohortMap.get(userId);
119
+ if (!cohortName) {
120
+ return; // This user is not in one of our defined cohorts, skip.
121
+ }
122
+
123
+ if (!todayPortfolio || !yesterdayPortfolio) {
124
+ return;
125
+ }
126
+
127
+ const yPos = this._getPortfolioPositions(yesterdayPortfolio);
128
+ const tPos = this._getPortfolioPositions(todayPortfolio);
129
+
130
+ // We must have AggregatedPositions for both days to do this calculation
131
+ if (!yPos || !tPos) {
132
+ return;
133
+ }
134
+
135
+ const yPosMap = new Map(yPos.map(p => [p.InstrumentID, p]));
136
+ const tPosMap = new Map(tPos.map(p => [p.InstrumentID, p]));
137
+ const allInstrumentIds = new Set([...yPosMap.keys(), ...tPosMap.keys()]);
138
+
139
+ for (const instrumentId of allInstrumentIds) {
140
+ if (!instrumentId) continue;
141
+
142
+ this._initFlowData(cohortName, instrumentId);
143
+ const asset = this.cohortFlows.get(cohortName).get(instrumentId);
144
+
145
+ const yP = yPosMap.get(instrumentId);
146
+ const tP = tPosMap.get(instrumentId);
147
+
148
+ // 'Invested' is the portfolio percentage (e.g., 5.0 = 5%)
149
+ const yInvested = yP?.Invested || 0;
150
+ const tInvested = tP?.Invested || 0;
151
+
152
+ if (yInvested > 0) {
153
+ asset.total_invested_yesterday += yInvested;
154
+ // 'NetProfit' here is actually the 1-day P&L *as a percentage* (e.g., 0.1 for +10%)
155
+ const yPriceChange = (yP?.NetProfit || 0);
156
+ asset.price_change_yesterday += yPriceChange * yInvested; // Weighted sum
157
+ }
158
+ if (tInvested > 0) {
159
+ asset.total_invested_today += tInvested;
160
+ }
161
+ }
162
+ }
163
+
164
+ async getResult() {
165
+ if (!this.mappings) {
166
+ this.mappings = await loadInstrumentMappings();
167
+ }
168
+
169
+ const finalResult = {};
170
+
171
+ for (const [cohortName, assetMap] of this.cohortFlows.entries()) {
172
+ const cohortAssets = {};
173
+ for (const [instrumentId, data] of assetMap.entries()) {
174
+ const ticker = this.mappings.instrumentToTicker[instrumentId];
175
+ if (!ticker) continue;
176
+
177
+ const { total_invested_yesterday, total_invested_today, price_change_yesterday } = data;
178
+
179
+ if (total_invested_yesterday > 0) {
180
+ // 1. Find the weighted average price change for this cohort/asset
181
+ // e.g., (0.1 * 5.0 + 0.05 * 2.0) / (5.0 + 2.0)
182
+ const avg_price_change_pct = price_change_yesterday / total_invested_yesterday;
183
+
184
+ // 2. Estimate yesterday's value *after* price change
185
+ // (This is what the value *would be* if no one bought or sold)
186
+ const price_adjusted_yesterday_value = total_invested_yesterday * (1 + avg_price_change_pct);
187
+
188
+ // 3. The difference between today's value and the price-adjusted
189
+ // value is the *net capital flow*.
190
+ const flow_contribution = total_invested_today - price_adjusted_yesterday_value;
191
+
192
+ // 4. Normalize the flow as a percentage of yesterday's capital
193
+ const net_flow_percentage = (flow_contribution / total_invested_yesterday) * 100;
194
+
195
+ if (isFinite(net_flow_percentage) && isFinite(flow_contribution)) {
196
+ cohortAssets[ticker] = {
197
+ net_flow_percentage: net_flow_percentage,
198
+ net_flow_contribution: flow_contribution // This is the %-point flow
199
+ };
200
+ }
201
+ }
202
+ }
203
+ finalResult[cohortName] = { assets: cohortAssets };
204
+ }
205
+ // Output is compact and not sharded
206
+ return finalResult;
207
+ }
208
+
209
+ reset() {
210
+ this.cohortFlows.clear();
211
+ this.cohortMap.clear();
212
+ this.mappings = null;
213
+ this.dependenciesLoaded = false;
214
+ }
215
+ }
216
+
217
+ module.exports = CohortCapitalFlow;
@@ -0,0 +1,211 @@
1
+ /**
2
+ * @fileoverview GAUSS Product Line (Pass 2)
3
+ *
4
+ * This 'standard' calculation re-streams all users, filters for
5
+ * the "Smart" and "Dumb" cohorts (from Pass 1), and then calculates
6
+ * their full 4D "Trader DNA".
7
+ *
8
+ * It then buckets these users into our final, named sub-cohorts
9
+ * (e.g., "Smart Investors", "FOMO Chasers").
10
+ */
11
+ const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
12
+
13
+ class CohortDefiner {
14
+ constructor() {
15
+ // We will store the full DNA for our filtered cohorts
16
+ this.smartVectors = []; // [ { userId, skill, time, fomo, bagholder }, ... ]
17
+ this.dumbVectors = []; // [ { userId, skill, time, fomo, bagholder }, ... ]
18
+
19
+ this.momentumData = null;
20
+ this.mappings = null;
21
+ this.cohortIdSets = null; // { smart: Set, dumb: Set }
22
+ }
23
+
24
+ /** Statically defines metadata */
25
+ static getMetadata() {
26
+ return {
27
+ type: 'standard',
28
+ rootDataDependencies: ['portfolio', 'history'],
29
+ isHistorical: true,
30
+ userType: 'all',
31
+ category: 'gauss'
32
+ };
33
+ }
34
+
35
+ /** Statically declare dependencies */
36
+ static getDependencies() {
37
+ return [
38
+ 'daily-dna-filter', // from gauss (Pass 1)
39
+ 'instrument-price-momentum-20d' // from core (Pass 1)
40
+ ];
41
+ }
42
+
43
+ /**
44
+ * Defines the output schema for this calculation.
45
+ */
46
+ static getSchema() {
47
+ const cohortSchema = {
48
+ "type": "array",
49
+ "description": "A list of User IDs belonging to this cohort.",
50
+ "items": { "type": "string" }
51
+ };
52
+
53
+ return {
54
+ "type": "object",
55
+ "description": "A map of named cohorts to the list of user IDs in each.",
56
+ "properties": {
57
+ "smart_investors": cohortSchema,
58
+ "smart_scalpers": cohortSchema,
59
+ "fomo_chasers": cohortSchema,
60
+ "patient_losers": cohortSchema,
61
+ "fomo_bagholders": cohortSchema
62
+ },
63
+ "additionalProperties": cohortSchema
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Loads dependencies into memory on the first process() call.
69
+ */
70
+ _loadDependencies(fetchedDependencies) {
71
+ if (this.cohortIdSets) return; // Run once
72
+
73
+ const dnaFilterData = fetchedDependencies['daily-dna-filter'];
74
+ this.momentumData = fetchedDependencies['instrument-price-momentum-20d'];
75
+
76
+ this.cohortIdSets = {
77
+ smart: new Set(dnaFilterData?.smart_cohort_ids || []),
78
+ dumb: new Set(dnaFilterData?.dumb_cohort_ids || [])
79
+ };
80
+ }
81
+
82
+ _getFomoScore(todayPortfolio, yesterdayPortfolio) {
83
+ if (!this.mappings) return 0;
84
+ const yIds = new Set((yesterdayPortfolio?.AggregatedPositions || []).map(p => p.InstrumentID));
85
+ const newPositions = (todayPortfolio?.AggregatedPositions || []).filter(p => p.InstrumentID && !yIds.has(p.InstrumentID));
86
+ if (newPositions.length === 0) return 0;
87
+
88
+ let fomoSum = 0;
89
+ let count = 0;
90
+ for (const pos of newPositions) {
91
+ const ticker = this.mappings.instrumentToTicker[pos.InstrumentID];
92
+ if (ticker && this.momentumData[ticker]) {
93
+ fomoSum += this.momentumData[ticker].momentum_20d_pct || 0;
94
+ count++;
95
+ }
96
+ }
97
+ return (count > 0) ? fomoSum / count : 0;
98
+ }
99
+
100
+ _getBagholderScore(todayPortfolio) {
101
+ const openPositions = todayPortfolio?.AggregatedPositions || [];
102
+ if (openPositions.length === 0) return 0;
103
+
104
+ let durationSum = 0;
105
+ let count = 0;
106
+ const now = new Date();
107
+
108
+ for (const pos of openPositions) {
109
+ // 'NetProfit' is a % P&L, e.g., -0.20 for -20%
110
+ if (pos.NetProfit < -0.20) {
111
+ try {
112
+ const openDate = new Date(pos.OpenDateTime);
113
+ const durationMs = now.getTime() - openDate.getTime();
114
+ const durationDays = durationMs / (1000 * 60 * 60 * 24);
115
+ if (isFinite(durationDays)) {
116
+ durationSum += durationDays;
117
+ count++;
118
+ }
119
+ } catch (e) { /* ignore invalid date */ }
120
+ }
121
+ }
122
+ return (count > 0) ? durationSum / count : 0;
123
+ }
124
+
125
+ process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights, fetchedDependencies) {
126
+ this._loadDependencies(fetchedDependencies);
127
+ if (!this.mappings) {
128
+ this.mappings = context.mappings;
129
+ }
130
+
131
+ const isSmart = this.cohortIdSets.smart.has(userId);
132
+ const isDumb = this.cohortIdSets.dumb.has(userId);
133
+
134
+ if (!isSmart && !isDumb) {
135
+ return; // Not in our extreme cohorts
136
+ }
137
+
138
+ // --- Re-calculate full DNA for this filtered user ---
139
+
140
+ // 1. Get Long-Term (Stable) Traits
141
+ const history = todayPortfolio?.history?.all;
142
+ if (!history) return;
143
+
144
+ const winRate = (history.winRatio || 0) / 100.0;
145
+ const lossRate = 1.0 - winRate;
146
+ const avgWin = history.avgProfitPct || 0;
147
+ const avgLoss = Math.abs(history.avgLossPct || 0);
148
+ const lt_skill = (winRate * avgWin) - (lossRate * avgLoss);
149
+ const lt_time = history.avgHoldingTimeInMinutes || 0;
150
+
151
+ // 2. Get Short-Term (Noisy) Traits
152
+ const st_fomo = this._getFomoScore(todayPortfolio, yesterdayPortfolio);
153
+ const st_bagholder = this._getBagholderScore(todayPortfolio);
154
+
155
+ const vector = { userId, skill: lt_skill, time: lt_time, fomo: st_fomo, bagholder: st_bagholder };
156
+
157
+ if (isSmart) {
158
+ this.smartVectors.push(vector);
159
+ } else if (isDumb) {
160
+ this.dumbVectors.push(vector);
161
+ }
162
+ }
163
+
164
+ _getMedian(vectors, key) {
165
+ if (vectors.length === 0) return 0;
166
+ const sorted = vectors.map(v => v[key]).sort((a, b) => a - b);
167
+ const mid = Math.floor(sorted.length / 2);
168
+ return sorted[mid];
169
+ }
170
+
171
+ getResult() {
172
+ const cohorts = {};
173
+
174
+ // 1. Process Smart Cohort
175
+ const smart_median_time = this._getMedian(this.smartVectors, 'time');
176
+ cohorts['smart_investors'] = this.smartVectors
177
+ .filter(u => u.time >= smart_median_time)
178
+ .map(u => u.userId);
179
+ cohorts['smart_scalpers'] = this.smartVectors
180
+ .filter(u => u.time < smart_median_time)
181
+ .map(u => u.userId);
182
+
183
+ // 2. Process Dumb Cohort
184
+ const dumb_median_fomo = this._getMedian(this.dumbVectors, 'fomo');
185
+ const dumb_median_bag = this._getMedian(this.dumbVectors, 'bagholder');
186
+
187
+ cohorts['fomo_chasers'] = this.dumbVectors
188
+ .filter(u => u.fomo >= dumb_median_fomo && u.bagholder < dumb_median_bag)
189
+ .map(u => u.userId);
190
+
191
+ cohorts['patient_losers'] = this.dumbVectors
192
+ .filter(u => u.fomo < dumb_median_fomo && u.bagholder >= dumb_median_bag)
193
+ .map(u => u.userId);
194
+
195
+ cohorts['fomo_bagholders'] = this.dumbVectors
196
+ .filter(u => u.fomo >= dumb_median_fomo && u.bagholder >= dumb_median_bag)
197
+ .map(u => u.userId);
198
+
199
+ // Output is a compact map of cohort_name -> [userIds]
200
+ return cohorts;
201
+ }
202
+
203
+ reset() {
204
+ this.smartVectors = [];
205
+ this.dumbVectors = [];
206
+ this.momentumData = null;
207
+ this.mappings = null;
208
+ this.cohortIdSets = null;
209
+ }
210
+ }
211
+ module.exports = CohortDefiner;
@@ -0,0 +1,130 @@
1
+ /**
2
+ * @fileoverview GAUSS Product Line (Pass 1)
3
+ *
4
+ * This metric is a *filter*. It processes all users and calculates
5
+ * their "Long-Term Skill" (Expectancy) from their 'history' doc.
6
+ *
7
+ * It then filters this list to *only* return the User IDs
8
+ * of the top 20% (Smart) and bottom 20% (Dumb) of users,
9
+ * solving the 1MB document limit for downstream processing.
10
+ */
11
+ class DailyDnaFilter {
12
+ constructor() {
13
+ // We will store all users and their primary skill score
14
+ // [ [userId, skillScore], ... ]
15
+ this.allUserSkills = [];
16
+ }
17
+
18
+ /**
19
+ * Statically defines all metadata for the manifest builder.
20
+ */
21
+ static getMetadata() {
22
+ return {
23
+ type: 'standard',
24
+ // This calc only needs the 'history' doc,
25
+ // which is passed inside the portfolio object.
26
+ rootDataDependencies: ['history'],
27
+ isHistorical: false, // Reads today's history snapshot
28
+ userType: 'all',
29
+ category: 'gauss'
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Statically declare dependencies.
35
+ */
36
+ static getDependencies() {
37
+ return []; // This is a Pass 1 calculation
38
+ }
39
+
40
+ /**
41
+ * Defines the output schema for this calculation.
42
+ */
43
+ static getSchema() {
44
+ return {
45
+ "type": "object",
46
+ "description": "Provides the User ID lists for the 'Smart' (top 20%) and 'Dumb' (bottom 20%) cohorts based on long-term Expectancy.",
47
+ "properties": {
48
+ "smart_cohort_ids": {
49
+ "type": "array",
50
+ "description": "List of user IDs in the top 20% 'Smart' cohort.",
51
+ "items": { "type": "string" }
52
+ },
53
+ "dumb_cohort_ids": {
54
+ "type": "array",
55
+ "description": "List of user IDs in the bottom 20% 'Dumb' cohort.",
56
+ "items": { "type": "string" }
57
+ },
58
+ "total_users_analyzed": { "type": "number" },
59
+ "cohort_size": { "type": "number" }
60
+ },
61
+ "required": ["smart_cohort_ids", "dumb_cohort_ids", "total_users_analyzed", "cohort_size"]
62
+ };
63
+ }
64
+
65
+
66
+ /**
67
+ * Processes a single user's daily data.
68
+ * @param {object} todayPortfolio - Today's portfolio snapshot.
69
+ * @param {object} yesterdayPortfolio - Yesterday's portfolio snapshot.
70
+ * @param {string} userId - The user's ID.
71
+ */
72
+ process(todayPortfolio, yesterdayPortfolio, userId) {
73
+ // 1. Get Long-Term (Stable) Traits from history
74
+ const history = todayPortfolio?.history?.all;
75
+ if (!history || !history.totalTrades || history.totalTrades < 20) {
76
+ return; // Not enough history for a reliable score
77
+ }
78
+
79
+ const winRate = (history.winRatio || 0) / 100.0;
80
+ const lossRate = 1.0 - winRate;
81
+ const avgWin = history.avgProfitPct || 0;
82
+ // Use Math.abs because avgLossPct is stored as a negative number
83
+ const avgLoss = Math.abs(history.avgLossPct || 0);
84
+
85
+ // Expectancy = (Win % * Avg Win %) - (Loss % * Avg Loss %)
86
+ const lt_skill = (winRate * avgWin) - (lossRate * avgLoss);
87
+
88
+ if (!isFinite(lt_skill)) {
89
+ return; // Skip bad data (e.g., division by zero)
90
+ }
91
+
92
+ this.allUserSkills.push([userId, lt_skill]);
93
+ }
94
+
95
+ /**
96
+ * Returns the final filtered list of user IDs.
97
+ */
98
+ getResult() {
99
+ const totalUsers = this.allUserSkills.length;
100
+ if (totalUsers === 0) {
101
+ return { smart_cohort_ids: [], dumb_cohort_ids: [], total_users_analyzed: 0, cohort_size: 0 };
102
+ }
103
+
104
+ // Sort by Skill (Expectancy) in descending order (highest skill first)
105
+ this.allUserSkills.sort((a, b) => b[1] - a[1]);
106
+
107
+ const cohortSize = Math.floor(totalUsers * 0.20); // Top/Bottom 20%
108
+ if (cohortSize === 0) {
109
+ return { smart_cohort_ids: [], dumb_cohort_ids: [], total_users_analyzed: totalUsers, cohort_size: 0 };
110
+ }
111
+
112
+ // Get the top 20% (smartest)
113
+ const smart_cohort_ids = this.allUserSkills.slice(0, cohortSize).map(u => u[0]);
114
+ // Get the bottom 20% (dumbest)
115
+ const dumb_cohort_ids = this.allUserSkills.slice(-cohortSize).map(u => u[0]);
116
+
117
+ // Output is now small and compact, solving the 1MB limit.
118
+ return {
119
+ smart_cohort_ids: smart_cohort_ids,
120
+ dumb_cohort_ids: dumb_cohort_ids,
121
+ total_users_analyzed: totalUsers,
122
+ cohort_size: cohortSize
123
+ };
124
+ }
125
+
126
+ reset() {
127
+ this.allUserSkills = [];
128
+ }
129
+ }
130
+ module.exports = DailyDnaFilter;