aiden-shared-calculations-unified 1.0.0 → 1.0.2
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 +78 -0
- package/calculations/capital_flow/deposit_withdrawal_percentage.js +71 -0
- package/calculations/capital_flow/new_allocation_percentage.js +49 -0
- package/calculations/capital_flow/reallocation_increase_percentage.js +63 -0
- package/calculations/insights/daily_bought_vs_sold_count.js +56 -0
- package/calculations/insights/daily_buy_sell_sentiment_count.js +50 -0
- package/calculations/insights/daily_ownership_delta.js +56 -0
- package/calculations/insights/daily_total_positions_held.js +40 -0
- package/package.json +1 -1
package/README.MD
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Unified Calculations Package (`aiden-shared-calculations-unified`)
|
|
2
|
+
|
|
3
|
+
**Version:** 1.0.0
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This package centralizes all data calculation logic for the BullTrackers project. It provides a standardized structure for calculations run by the unified Computation System and includes shared utility functions. Calculations are dynamically loaded and categorized.
|
|
8
|
+
|
|
9
|
+
## Package Structure
|
|
10
|
+
|
|
11
|
+
### `/utils`
|
|
12
|
+
|
|
13
|
+
Shared utility functions used by calculations or the systems consuming them.
|
|
14
|
+
|
|
15
|
+
* `firestore_utils.js`: Provides a resilient `withRetry` wrapper for Firestore operations using exponential backoff.
|
|
16
|
+
* `sector_mapping_provider.js`: Provides functions (`loadInstrumentMappings`, `getInstrumentSectorMap`) to fetch and cache instrument-to-ticker and instrument-to-sector mappings from Firestore.
|
|
17
|
+
|
|
18
|
+
### `/calculations`
|
|
19
|
+
|
|
20
|
+
Contains the core calculation logic, organized into subdirectories representing categories.
|
|
21
|
+
|
|
22
|
+
* **Calculation Class Structure:** Each `.js` file defines a class responsible for a specific metric. Every class **must** implement:
|
|
23
|
+
* `constructor()`: Initializes any internal state needed for aggregation.
|
|
24
|
+
* `process(portfolioData, userId, context)` OR `process(todayPortfolio, yesterdayPortfolio, userId)`: Processes a single user's data. The signature depends on whether the calculation requires historical comparison. `context` provides shared data like mappings.
|
|
25
|
+
* `getResult()`: Returns the final, calculated result for the aggregation period. **Crucially, this method must perform any final averaging (e.g., sum/count) itself.** It should return the final value or object ready for storage, not raw components.
|
|
26
|
+
* `reset()`: (Optional but recommended) Resets the internal state, often used by the calling system between processing batches or days.
|
|
27
|
+
|
|
28
|
+
* **Categories (Examples based on your files):**
|
|
29
|
+
* `/asset_metrics`: Calculations focused on individual assets (e.g., `asset_dollar_metrics`, `asset_position_size`).
|
|
30
|
+
* `/behavioural`: Calculations analyzing user trading patterns (e.g., `drawdown_response`, `gain_response`, `paper_vs_diamond_hands`, `position_count_pnl`, `smart_money_flow`).
|
|
31
|
+
* `/pnl`: Profit and Loss related calculations (e.g., `asset_pnl_status`, `average_daily_pnl_all_users`, `average_daily_pnl_per_sector`, `average_daily_pnl_per_stock`, `average_daily_position_pnl`, `pnl_distribution_per_stock`, `profitability_migration`, `profitability_ratio_per_stock`, `profitability_skew_per_stock`, `user_profitability_tracker`).
|
|
32
|
+
* `/sanity`: Basic checks and counts (e.g., `users_processed`).
|
|
33
|
+
* `/sectors`: Calculations aggregated by market sector (e.g., `diversification_pnl`, `sector_dollar_metrics`, `sector_rotation`, `total_long_per_sector`, `total_short_per_sector`).
|
|
34
|
+
* `/sentiment`: Calculations related to market sentiment (e.g., `crowd_conviction_score`).
|
|
35
|
+
* `/short_and_long_stats`: Specific counts for short and long positions (e.g., `long_position_per_stock`, `sentiment_per_stock`, `short_position_per_stock`, `total_long_figures`, `total_short_figures`).
|
|
36
|
+
* `/speculators`: Calculations **specifically** for the 'speculator' user type, often involving leverage, stop-loss, or take-profit data (e.g., `distance_to_stop_loss_per_leverage`, `distance_to_tp_per_leverage`, `entry_distance_to_sl_per_leverage`, `entry_distance_to_tp_per_leverage`, `holding_duration_per_asset`, `leverage_per_asset`, `leverage_per_sector`, `risk_appetite_change`, `risk_reward_ratio_per_asset`, `speculator_asset_sentiment`, `speculator_danger_zone`, `stop_loss_distance_by_sector_short_long_breakdown`, `stop_loss_distance_by_ticker_short_long_breakdown`, `stop_loss_per_asset`, `take_profit_per_asset`, `tsl_effectiveness`, `tsl_per_asset`).
|
|
37
|
+
|
|
38
|
+
* **Output Formats:** Calculations should adhere to the standardized output formats defined in `docs/Notes/output_formats.md`.
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
This package is intended to be consumed primarily by the unified Computation System.
|
|
43
|
+
|
|
44
|
+
```javascript
|
|
45
|
+
// Example usage within Computation System
|
|
46
|
+
const { calculations, utils } = require('aiden-shared-calculations-unified');
|
|
47
|
+
|
|
48
|
+
const CalculationClass = calculations.pnl.average_daily_pnl_per_stock;
|
|
49
|
+
const calculator = new CalculationClass();
|
|
50
|
+
|
|
51
|
+
// ... load data ...
|
|
52
|
+
|
|
53
|
+
// In a loop for each user:
|
|
54
|
+
calculator.process(portfolioData, userId, context);
|
|
55
|
+
|
|
56
|
+
// After processing all users:
|
|
57
|
+
const results = await calculator.getResult();
|
|
58
|
+
|
|
59
|
+
// ... store results ...
|
|
60
|
+
````
|
|
61
|
+
|
|
62
|
+
## Contributing
|
|
63
|
+
|
|
64
|
+
*(Outline the process for adding new calculations)*
|
|
65
|
+
|
|
66
|
+
1. **Determine Category:** Decide which subdirectory (`/calculations/<category>`) the new metric belongs to. Create a new category if necessary.
|
|
67
|
+
2. **Create File:** Create a new `.js` file using kebab-case (e.g., `my-new-metric.js`).
|
|
68
|
+
3. **Implement Class:** Define the calculation class, ensuring it has `constructor`, `process`, and `getResult` methods adhering to the standards. Remember `getResult` must return the *final* computed value.
|
|
69
|
+
4. **Add to Manifest:** The main `index.js` uses `require-all`, so the new calculation should be automatically included in the exports upon the next package build/publish, assuming the file naming and structure are correct.
|
|
70
|
+
5. **Publish:** Bump the package version (`npm version patch` or minor/major as appropriate) and publish (`npm publish --access public`).
|
|
71
|
+
6. **Update Consumer:** Update the version dependency in the Computation System's `package.json`.
|
|
72
|
+
|
|
73
|
+
<!-- end list -->
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
```
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Estimates the average percentage deposited into accounts between two days.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
class DepositPercentage {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.totalDepositPercentage = 0;
|
|
8
|
+
this.userCount = 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Calculates total PnL from aggregated or public positions.
|
|
13
|
+
* @param {object} portfolio - Portfolio data object.
|
|
14
|
+
* @returns {number|null} Total PnL or null if positions are missing.
|
|
15
|
+
*/
|
|
16
|
+
calculateTotalPnl(portfolio) {
|
|
17
|
+
// Use the same logic as in ProfitabilityMigration
|
|
18
|
+
if (portfolio && portfolio.AggregatedPositions) {
|
|
19
|
+
return portfolio.AggregatedPositions.reduce((sum, pos) => sum + (pos.NetProfit || 0), 0);
|
|
20
|
+
} else if (portfolio && portfolio.PublicPositions) {
|
|
21
|
+
// PublicPositions might lack NetProfit, adjust if necessary based on your data
|
|
22
|
+
return portfolio.PublicPositions.reduce((sum, pos) => sum + (pos.NetProfit || 0), 0);
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
process(todayPortfolio, yesterdayPortfolio, userId) {
|
|
28
|
+
if (!todayPortfolio || !yesterdayPortfolio || !yesterdayPortfolio.PortfolioValue || yesterdayPortfolio.PortfolioValue === 0) {
|
|
29
|
+
// Need both portfolios and a non-zero yesterday value for calculation
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const valueChange = (todayPortfolio.PortfolioValue || 0) - yesterdayPortfolio.PortfolioValue;
|
|
34
|
+
|
|
35
|
+
const todayPnl = this.calculateTotalPnl(todayPortfolio);
|
|
36
|
+
const yesterdayPnl = this.calculateTotalPnl(yesterdayPortfolio);
|
|
37
|
+
|
|
38
|
+
// If PnL can't be calculated for either day, we can't reliably estimate cash flow
|
|
39
|
+
if (todayPnl === null || yesterdayPnl === null) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const pnlChange = todayPnl - yesterdayPnl;
|
|
44
|
+
const cashFlow = valueChange - pnlChange;
|
|
45
|
+
|
|
46
|
+
// Check for deposit (positive cash flow)
|
|
47
|
+
if (cashFlow > 0) {
|
|
48
|
+
const depositPercentage = (cashFlow / yesterdayPortfolio.PortfolioValue) * 100;
|
|
49
|
+
this.totalDepositPercentage += depositPercentage;
|
|
50
|
+
this.userCount++;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
getResult() {
|
|
55
|
+
if (this.userCount === 0) {
|
|
56
|
+
return {}; // Return empty object if no users contributed
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
// Calculate the final average directly
|
|
61
|
+
average_deposit_percentage: this.totalDepositPercentage / this.userCount
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
reset() {
|
|
66
|
+
this.totalDepositPercentage = 0;
|
|
67
|
+
this.userCount = 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = DepositPercentage;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Calculates the average percentage of total portfolio value
|
|
3
|
+
* newly allocated to assets not held on the previous day.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class NewAllocationPercentage {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.accumulatedNewAllocationPercentage = 0;
|
|
9
|
+
this.userCount = 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
process(todayPortfolio, yesterdayPortfolio, userId) {
|
|
13
|
+
const todayPositions = todayPortfolio?.AggregatedPositions;
|
|
14
|
+
const yesterdayPositions = yesterdayPortfolio?.AggregatedPositions || [];
|
|
15
|
+
|
|
16
|
+
if (!todayPositions || todayPositions.length === 0) return;
|
|
17
|
+
|
|
18
|
+
const yesterdayIds = new Set(yesterdayPositions.map(p => p.InstrumentID));
|
|
19
|
+
let userNewAllocation = 0;
|
|
20
|
+
|
|
21
|
+
for (const pos of todayPositions) {
|
|
22
|
+
if (!yesterdayIds.has(pos.InstrumentID)) {
|
|
23
|
+
const invested = typeof pos.Invested === 'number' ? pos.Invested : 0;
|
|
24
|
+
userNewAllocation += invested;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Guard against data rounding or eToro API drift
|
|
29
|
+
if (userNewAllocation > 100) userNewAllocation = 100;
|
|
30
|
+
|
|
31
|
+
this.accumulatedNewAllocationPercentage += userNewAllocation;
|
|
32
|
+
this.userCount++;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getResult() {
|
|
36
|
+
if (this.userCount === 0) return {};
|
|
37
|
+
return {
|
|
38
|
+
average_new_allocation_percentage:
|
|
39
|
+
this.accumulatedNewAllocationPercentage / this.userCount
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
reset() {
|
|
44
|
+
this.accumulatedNewAllocationPercentage = 0;
|
|
45
|
+
this.userCount = 0;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = NewAllocationPercentage;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Calculates the average percentage increase in allocation
|
|
3
|
+
* specifically towards assets already held on the previous day.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class ReallocationIncreasePercentage {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.accumulatedIncreasePercentage = 0;
|
|
9
|
+
this.userCount = 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
process(todayPortfolio, yesterdayPortfolio, userId) {
|
|
13
|
+
if (!todayPortfolio || !yesterdayPortfolio || !todayPortfolio.AggregatedPositions || !yesterdayPortfolio.AggregatedPositions) {
|
|
14
|
+
// Requires AggregatedPositions which contain the 'Invested' percentage
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const yesterdayPositions = new Map(yesterdayPortfolio.AggregatedPositions.map(p => [p.InstrumentID, p]));
|
|
19
|
+
let userTotalIncreasePercentage = 0;
|
|
20
|
+
|
|
21
|
+
for (const todayPos of todayPortfolio.AggregatedPositions) {
|
|
22
|
+
const yesterdayPos = yesterdayPositions.get(todayPos.InstrumentID);
|
|
23
|
+
|
|
24
|
+
// Check if the asset was held yesterday
|
|
25
|
+
if (yesterdayPos) {
|
|
26
|
+
// Ensure 'Invested' property exists and is a number
|
|
27
|
+
const todayInvested = typeof todayPos.Invested === 'number' ? todayPos.Invested : 0;
|
|
28
|
+
const yesterdayInvested = typeof yesterdayPos.Invested === 'number' ? yesterdayPos.Invested : 0;
|
|
29
|
+
|
|
30
|
+
const deltaInvested = todayInvested - yesterdayInvested;
|
|
31
|
+
|
|
32
|
+
// Accumulate only the increases
|
|
33
|
+
if (deltaInvested > 0) {
|
|
34
|
+
userTotalIncreasePercentage += deltaInvested;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Only count users who had positions on both days for this metric
|
|
40
|
+
if (yesterdayPortfolio.AggregatedPositions.length > 0 && todayPortfolio.AggregatedPositions.length > 0) {
|
|
41
|
+
this.accumulatedIncreasePercentage += userTotalIncreasePercentage;
|
|
42
|
+
this.userCount++;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
getResult() {
|
|
47
|
+
if (this.userCount === 0) {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
// Calculate the final average directly
|
|
53
|
+
average_reallocation_increase_percentage: this.accumulatedIncreasePercentage / this.userCount
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
reset() {
|
|
58
|
+
this.accumulatedIncreasePercentage = 0;
|
|
59
|
+
this.userCount = 0;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = ReallocationIncreasePercentage;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Calculates the total number of positions 'bought' vs 'sold' based on daily owner delta.
|
|
3
|
+
* Uses the 'daily_instrument_insights' collection (truth of source).
|
|
4
|
+
* 'Bought' = sum of positive deltas, 'Sold' = sum of absolute negative deltas.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class DailyBoughtVsSoldCount {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.totalBought = 0;
|
|
10
|
+
this.totalSold = 0;
|
|
11
|
+
this.processedDay = false; // Flag to run logic only once per day
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Needs insights from today and yesterday
|
|
15
|
+
async process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights) {
|
|
16
|
+
if (this.processedDay) return;
|
|
17
|
+
this.processedDay = true;
|
|
18
|
+
|
|
19
|
+
if (!todayInsights || !todayInsights.insights || !yesterdayInsights || !yesterdayInsights.insights) {
|
|
20
|
+
console.warn('[DailyBoughtVsSoldCount] Missing insights data for today or yesterday.');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const yesterdayTotals = new Map(yesterdayInsights.insights.map(i => [i.instrumentId, i.total]));
|
|
25
|
+
|
|
26
|
+
for (const instrument of todayInsights.insights) {
|
|
27
|
+
const instrumentId = instrument.instrumentId;
|
|
28
|
+
const todayTotal = instrument.total || 0;
|
|
29
|
+
const yesterdayTotal = yesterdayTotals.get(instrumentId) || 0;
|
|
30
|
+
|
|
31
|
+
const delta = todayTotal - yesterdayTotal;
|
|
32
|
+
|
|
33
|
+
if (delta > 0) {
|
|
34
|
+
this.totalBought += delta;
|
|
35
|
+
} else if (delta < 0) {
|
|
36
|
+
this.totalSold += Math.abs(delta); // Sum of absolute decreases
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getResult() {
|
|
42
|
+
// Return the final aggregated counts
|
|
43
|
+
return {
|
|
44
|
+
total_positions_bought_delta: this.totalBought,
|
|
45
|
+
total_positions_sold_delta: this.totalSold
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
reset() {
|
|
50
|
+
this.totalBought = 0;
|
|
51
|
+
this.totalSold = 0;
|
|
52
|
+
this.processedDay = false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = DailyBoughtVsSoldCount;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Calculates the total number of 'buy' vs 'sell' (short) positions across all instruments.
|
|
3
|
+
* Uses the 'daily_instrument_insights' collection (truth of source).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class DailyBuySellSentimentCount {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.totalBuyPositions = 0;
|
|
9
|
+
this.totalSellPositions = 0;
|
|
10
|
+
this.processedDay = false; // Flag to run logic only once per day
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Only needs today's insights
|
|
14
|
+
async process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights) {
|
|
15
|
+
if (this.processedDay) return;
|
|
16
|
+
this.processedDay = true;
|
|
17
|
+
|
|
18
|
+
if (!todayInsights || !todayInsights.insights) {
|
|
19
|
+
console.warn('[DailyBuySellSentimentCount] Missing insights data for today.');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (const instrument of todayInsights.insights) {
|
|
24
|
+
const totalOwners = instrument.total || 0;
|
|
25
|
+
if (totalOwners > 0) {
|
|
26
|
+
const buyPercent = (instrument.buy || 0) / 100;
|
|
27
|
+
const sellPercent = (instrument.sell || 0) / 100; // 'sell' means short here
|
|
28
|
+
|
|
29
|
+
this.totalBuyPositions += (buyPercent * totalOwners);
|
|
30
|
+
this.totalSellPositions += (sellPercent * totalOwners);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getResult() {
|
|
36
|
+
// Return the final rounded sums
|
|
37
|
+
return {
|
|
38
|
+
total_buy_positions: Math.round(this.totalBuyPositions),
|
|
39
|
+
total_sell_positions: Math.round(this.totalSellPositions) // 'sell' are shorts
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
reset() {
|
|
44
|
+
this.totalBuyPositions = 0;
|
|
45
|
+
this.totalSellPositions = 0;
|
|
46
|
+
this.processedDay = false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = DailyBuySellSentimentCount;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Calculates the daily change (delta) in total owners for each instrument.
|
|
3
|
+
* Uses the 'daily_instrument_insights' collection (truth of source).
|
|
4
|
+
*/
|
|
5
|
+
const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
|
|
6
|
+
|
|
7
|
+
class DailyOwnershipDelta {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.deltaByTicker = {};
|
|
10
|
+
this.mappings = null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// This calculation only needs insights data, not portfolio data.
|
|
14
|
+
// It runs daily but needs 'yesterday's insights to calculate the delta.
|
|
15
|
+
async process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights) {
|
|
16
|
+
// We only need to run the core logic once per day, not per user.
|
|
17
|
+
// Use a flag to ensure it runs only on the first user processed for the day.
|
|
18
|
+
if (this.processedDay) return;
|
|
19
|
+
this.processedDay = true; // Set flag
|
|
20
|
+
|
|
21
|
+
if (!todayInsights || !todayInsights.insights || !yesterdayInsights || !yesterdayInsights.insights) {
|
|
22
|
+
console.warn('[DailyOwnershipDelta] Missing insights data for today or yesterday.');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!this.mappings) {
|
|
27
|
+
this.mappings = await loadInstrumentMappings();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const yesterdayTotals = new Map(yesterdayInsights.insights.map(i => [i.instrumentId, i.total]));
|
|
31
|
+
|
|
32
|
+
for (const instrument of todayInsights.insights) {
|
|
33
|
+
const instrumentId = instrument.instrumentId;
|
|
34
|
+
const todayTotal = instrument.total || 0;
|
|
35
|
+
const yesterdayTotal = yesterdayTotals.get(instrumentId) || 0; // Default to 0 if not found yesterday
|
|
36
|
+
|
|
37
|
+
const delta = todayTotal - yesterdayTotal;
|
|
38
|
+
const ticker = this.mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
|
|
39
|
+
|
|
40
|
+
this.deltaByTicker[ticker] = delta;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
getResult() {
|
|
45
|
+
// Return the final calculated delta for each ticker
|
|
46
|
+
return this.deltaByTicker;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
reset() {
|
|
50
|
+
this.deltaByTicker = {};
|
|
51
|
+
this.processedDay = false; // Reset the flag for the next day
|
|
52
|
+
this.mappings = null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = DailyOwnershipDelta;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Calculates the total number of positions held across all instruments for the day.
|
|
3
|
+
* Uses the 'daily_instrument_insights' collection (truth of source).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class DailyTotalPositionsHeld {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.totalPositions = 0;
|
|
9
|
+
this.processedDay = false; // Flag to run logic only once per day
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Only needs today's insights
|
|
13
|
+
async process(todayPortfolio, yesterdayPortfolio, userId, context, todayInsights, yesterdayInsights) {
|
|
14
|
+
if (this.processedDay) return;
|
|
15
|
+
this.processedDay = true;
|
|
16
|
+
|
|
17
|
+
if (!todayInsights || !todayInsights.insights) {
|
|
18
|
+
console.warn('[DailyTotalPositionsHeld] Missing insights data for today.');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
for (const instrument of todayInsights.insights) {
|
|
23
|
+
this.totalPositions += (instrument.total || 0);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
getResult() {
|
|
28
|
+
// Return the final sum
|
|
29
|
+
return {
|
|
30
|
+
total_positions_held: this.totalPositions
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
reset() {
|
|
35
|
+
this.totalPositions = 0;
|
|
36
|
+
this.processedDay = false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = DailyTotalPositionsHeld;
|