aiden-shared-calculations-unified 1.0.68 → 1.0.69
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/{speculators → behavioural/historical}/holding_duration_per_asset.js +39 -34
- package/calculations/behavioural/overall_holding_duration.js +79 -0
- package/calculations/insights/daily_buy_sell_sentiment_count.js +36 -26
- package/calculations/insights/daily_ownership_per_sector.js +87 -0
- package/calculations/insights/daily_total_positions_held.js +30 -19
- package/calculations/insights/historical/daily_ownership_delta.js +46 -57
- package/calculations/pnl/overall_profitability_ratio.js +71 -0
- package/calculations/pnl/pnl_distribution_per_stock.js +93 -41
- package/calculations/pnl/profitability_ratio_per_sector,js +104 -0
- package/calculations/sectors/historical/diversification_pnl.js +64 -88
- package/calculations/speculators/historical/risk_appetite_change.js +60 -77
- package/package.json +1 -1
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Calculation (Pass 1) for
|
|
2
|
+
* @fileoverview Calculation (Pass 1) for user behaviour.
|
|
3
3
|
*
|
|
4
|
-
* This metric answers: "What is the average holding duration
|
|
5
|
-
* (in hours) for
|
|
4
|
+
* REFACTOR: This metric now answers: "What is the average holding duration
|
|
5
|
+
* (in hours) for *closed* positions, averaged across all users, grouped by asset?"
|
|
6
|
+
*
|
|
7
|
+
* This calculation now uses the 'history' data source, not 'portfolio'.
|
|
6
8
|
*/
|
|
7
|
-
const { loadInstrumentMappings } = require('
|
|
9
|
+
const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
|
|
8
10
|
|
|
9
11
|
class HoldingDurationPerAsset {
|
|
10
12
|
constructor() {
|
|
11
13
|
// { [instrumentId]: { sum_hours: 0, count: 0 } }
|
|
14
|
+
// 'sum_hours' will be the sum of user-level averages
|
|
15
|
+
// 'count' will be the number of users who traded that asset
|
|
12
16
|
this.assets = new Map();
|
|
13
17
|
this.mappings = null;
|
|
14
18
|
}
|
|
@@ -23,11 +27,11 @@ class HoldingDurationPerAsset {
|
|
|
23
27
|
"properties": {
|
|
24
28
|
"avg_duration_hours": {
|
|
25
29
|
"type": "number",
|
|
26
|
-
"description": "Average holding duration in hours."
|
|
30
|
+
"description": "Average holding duration in hours (averaged across all users)."
|
|
27
31
|
},
|
|
28
32
|
"count": {
|
|
29
33
|
"type": "number",
|
|
30
|
-
"description": "Count of
|
|
34
|
+
"description": "Count of users used in average."
|
|
31
35
|
}
|
|
32
36
|
},
|
|
33
37
|
"required": ["avg_duration_hours", "count"]
|
|
@@ -35,7 +39,7 @@ class HoldingDurationPerAsset {
|
|
|
35
39
|
|
|
36
40
|
return {
|
|
37
41
|
"type": "object",
|
|
38
|
-
"description": "Calculates the average holding duration (in hours) for
|
|
42
|
+
"description": "Calculates the average holding duration (in hours) for closed positions per asset, averaged across all users.",
|
|
39
43
|
"patternProperties": {
|
|
40
44
|
"^.*$": tickerSchema // Ticker
|
|
41
45
|
},
|
|
@@ -48,40 +52,40 @@ class HoldingDurationPerAsset {
|
|
|
48
52
|
this.assets.set(instrumentId, { sum_hours: 0, count: 0 });
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
|
-
|
|
52
|
-
_getHoldingDurationHours(openDateStr) {
|
|
53
|
-
if (!openDateStr) return 0;
|
|
54
|
-
try {
|
|
55
|
-
const openDate = new Date(openDateStr);
|
|
56
|
-
// Get difference from 'now' (or the snapshot time)
|
|
57
|
-
const diffMs = new Date().getTime() - openDate.getTime();
|
|
58
|
-
return diffMs / (1000 * 60 * 60); // Convert ms to hours
|
|
59
|
-
} catch (e) {
|
|
60
|
-
return 0;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
55
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Process data from the 'history' root data source.
|
|
58
|
+
* @param {object} rootData - The root data object from the runner.
|
|
59
|
+
* @param {string} userId - The user ID.
|
|
60
|
+
*/
|
|
61
|
+
process(rootData, userId) {
|
|
62
|
+
// 1. Get the history data
|
|
63
|
+
const historyData = rootData.history;
|
|
64
|
+
if (!historyData || !Array.isArray(historyData.assets)) {
|
|
71
65
|
return;
|
|
72
66
|
}
|
|
73
67
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
68
|
+
// 2. Iterate over the aggregated assets in the history doc
|
|
69
|
+
for (const asset of historyData.assets) {
|
|
70
|
+
const instrumentId = asset.instrumentId;
|
|
71
|
+
|
|
72
|
+
// Skip the "all" aggregate entry
|
|
73
|
+
if (!instrumentId || instrumentId === -1) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
79
76
|
|
|
80
|
-
const
|
|
77
|
+
const durationMinutes = asset.avgHoldingTimeInMinutes;
|
|
81
78
|
|
|
82
|
-
if (
|
|
79
|
+
if (typeof durationMinutes === 'number' && durationMinutes > 0) {
|
|
80
|
+
this._initAsset(instrumentId);
|
|
81
|
+
|
|
83
82
|
const assetData = this.assets.get(instrumentId);
|
|
84
|
-
|
|
83
|
+
|
|
84
|
+
// Convert minutes to hours and add to the sum
|
|
85
|
+
assetData.sum_hours += (durationMinutes / 60);
|
|
86
|
+
|
|
87
|
+
// Increment count (this now counts *users* who have
|
|
88
|
+
// an average for this asset, not individual positions)
|
|
85
89
|
assetData.count++;
|
|
86
90
|
}
|
|
87
91
|
}
|
|
@@ -98,6 +102,7 @@ class HoldingDurationPerAsset {
|
|
|
98
102
|
|
|
99
103
|
if (data.count > 0) {
|
|
100
104
|
result[ticker] = {
|
|
105
|
+
// Calculate the final average (avg of avgs)
|
|
101
106
|
avg_duration_hours: data.sum_hours / data.count,
|
|
102
107
|
count: data.count
|
|
103
108
|
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Calculation (Pass 1) for user behaviour.
|
|
3
|
+
*
|
|
4
|
+
* This metric answers: "What is the average holding duration (in hours)
|
|
5
|
+
* for *closed* positions, averaged across all users?"
|
|
6
|
+
*
|
|
7
|
+
* It uses the 'history' data source's 'all' object.
|
|
8
|
+
*/
|
|
9
|
+
class AverageHoldingDurationOverall {
|
|
10
|
+
constructor() {
|
|
11
|
+
// Stores all the user-level average durations
|
|
12
|
+
this.durationsHours = [];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Defines the output schema for this calculation.
|
|
17
|
+
* @returns {object} JSON Schema object
|
|
18
|
+
*/
|
|
19
|
+
static getSchema() {
|
|
20
|
+
return {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"description": "Calculates the platform-wide average holding duration (in hours) for closed positions.",
|
|
23
|
+
"properties": {
|
|
24
|
+
"average_duration_hours": {
|
|
25
|
+
"type": "number",
|
|
26
|
+
"description": "The average holding duration in hours, averaged across all users."
|
|
27
|
+
},
|
|
28
|
+
"user_count": {
|
|
29
|
+
"type": "number",
|
|
30
|
+
"description": "The number of users included in the average."
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"required": ["average_duration_hours", "user_count"]
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Process data from the 'history' root data source.
|
|
39
|
+
* @param {object} rootData - The root data object from the runner.
|
|
40
|
+
* @param {string} userId - The user ID.
|
|
41
|
+
*/
|
|
42
|
+
process(rootData, userId) {
|
|
43
|
+
// 1. Get the history data
|
|
44
|
+
const historyData = rootData.history;
|
|
45
|
+
if (!historyData || !historyData.all) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 2. Get the user's overall average holding time
|
|
50
|
+
const durationMinutes = historyData.all.avgHoldingTimeInMinutes;
|
|
51
|
+
|
|
52
|
+
if (typeof durationMinutes === 'number' && durationMinutes > 0) {
|
|
53
|
+
this.durationsHours.push(durationMinutes / 60);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getResult() {
|
|
58
|
+
const count = this.durationsHours.length;
|
|
59
|
+
if (count === 0) {
|
|
60
|
+
return {
|
|
61
|
+
average_duration_hours: 0,
|
|
62
|
+
user_count: 0
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const sum = this.durationsHours.reduce((a, b) => a + b, 0);
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
average_duration_hours: sum / count,
|
|
70
|
+
user_count: count
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
reset() {
|
|
75
|
+
this.durationsHours = [];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = AverageHoldingDurationOverall;
|
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Calculation (Pass 1) for daily buy/sell sentiment.
|
|
3
3
|
*
|
|
4
|
-
* This
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* This provides a raw, global sentiment reading.
|
|
4
|
+
* REFACTOR: This is now a 'type: "meta"' calculation. It runs ONCE.
|
|
5
|
+
* It reads the pre-aggregated 'insights' data source and sums the
|
|
6
|
+
* 'buy' and 'sell' counts from all instruments.
|
|
8
7
|
*/
|
|
9
8
|
class DailyBuySellSentimentCount {
|
|
10
|
-
constructor() {
|
|
11
|
-
this.buyPositions = 0;
|
|
12
|
-
this.sellPositions = 0;
|
|
13
|
-
}
|
|
14
9
|
|
|
15
10
|
/**
|
|
16
11
|
* Defines the output schema for this calculation.
|
|
@@ -38,34 +33,49 @@ class DailyBuySellSentimentCount {
|
|
|
38
33
|
};
|
|
39
34
|
}
|
|
40
35
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
/**
|
|
37
|
+
* This is a 'meta' calculation. It runs once.
|
|
38
|
+
* @param {string} dateStr - The date string 'YYYY-MM-DD'.
|
|
39
|
+
* @param {object} rootData - The root data object. We expect rootData.insights.
|
|
40
|
+
* @param {object} dependencies - The shared dependencies (e.g., logger).
|
|
41
|
+
* @returns {object} The calculation result.
|
|
42
|
+
*/
|
|
43
|
+
process(dateStr, rootData, dependencies) {
|
|
44
|
+
let totalBuyPositions = 0;
|
|
45
|
+
let totalSellPositions = 0;
|
|
46
|
+
|
|
47
|
+
// rootData.insights contains the document from /daily_instrument_insights/
|
|
48
|
+
const insightsDoc = rootData.insights;
|
|
49
|
+
|
|
50
|
+
if (!insightsDoc || !Array.isArray(insightsDoc.insights)) {
|
|
51
|
+
dependencies.logger.log('WARN', `[daily-buy-sell-sentiment-count] No 'insights' data found for ${dateStr}.`);
|
|
52
|
+
return {
|
|
53
|
+
totalBuyPositions: 0,
|
|
54
|
+
totalSellPositions: 0,
|
|
55
|
+
sentimentRatio: null
|
|
56
|
+
};
|
|
45
57
|
}
|
|
46
58
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
// Iterate over the pre-aggregated array
|
|
60
|
+
for (const instrument of insightsDoc.insights) {
|
|
61
|
+
// The doc has pre-aggregated 'buy' and 'sell' counts
|
|
62
|
+
if (typeof instrument.buy === 'number') {
|
|
63
|
+
totalBuyPositions += instrument.buy;
|
|
64
|
+
}
|
|
65
|
+
if (typeof instrument.sell === 'number') {
|
|
66
|
+
totalSellPositions += instrument.sell;
|
|
52
67
|
}
|
|
53
68
|
}
|
|
54
|
-
}
|
|
55
69
|
|
|
56
|
-
getResult() {
|
|
57
70
|
return {
|
|
58
|
-
totalBuyPositions:
|
|
59
|
-
totalSellPositions:
|
|
71
|
+
totalBuyPositions: totalBuyPositions,
|
|
72
|
+
totalSellPositions: totalSellPositions,
|
|
60
73
|
// Calculate ratio: Buy / Sell
|
|
61
|
-
sentimentRatio: (
|
|
74
|
+
sentimentRatio: (totalSellPositions > 0) ? (totalBuyPositions / totalSellPositions) : null
|
|
62
75
|
};
|
|
63
76
|
}
|
|
64
77
|
|
|
65
|
-
reset()
|
|
66
|
-
this.buyPositions = 0;
|
|
67
|
-
this.sellPositions = 0;
|
|
68
|
-
}
|
|
78
|
+
// No constructor, getResult(), or reset() methods are needed for 'meta' calcs
|
|
69
79
|
}
|
|
70
80
|
|
|
71
81
|
module.exports = DailyBuySellSentimentCount;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Calculation (Pass 1) for daily ownership per sector.
|
|
3
|
+
*
|
|
4
|
+
* This is a 'type: "meta"' calculation. It runs ONCE.
|
|
5
|
+
* It reads the pre-aggregated 'insights' data source (/daily_instrument_insights/)
|
|
6
|
+
* and uses the sector mapping provider to aggregate the total number
|
|
7
|
+
* of owners for each sector.
|
|
8
|
+
*/
|
|
9
|
+
const { loadInstrumentMappings } = require('../../utils/sector_mapping_provider');
|
|
10
|
+
|
|
11
|
+
class DailyOwnershipPerSector {
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Defines the output schema for this calculation.
|
|
15
|
+
* @returns {object} JSON Schema object
|
|
16
|
+
*/
|
|
17
|
+
static getSchema() {
|
|
18
|
+
const sectorSchema = {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"description": "Aggregated ownership for a single sector.",
|
|
21
|
+
"properties": {
|
|
22
|
+
"total_owners": {
|
|
23
|
+
"type": "number",
|
|
24
|
+
"description": "The total number of unique owners for all assets in this sector."
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"required": ["total_owners"]
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"description": "Calculates the total unique owners per sector based on the 'insights' data source.",
|
|
33
|
+
"patternProperties": {
|
|
34
|
+
"^.*$": sectorSchema // Matches any string key (sector name)
|
|
35
|
+
},
|
|
36
|
+
"additionalProperties": sectorSchema
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* This is a 'meta' calculation. It runs once.
|
|
42
|
+
* @param {string} dateStr - The date string 'YYYY-MM-DD'.
|
|
43
|
+
* @param {object} rootData - The root data object. We expect rootData.insights.
|
|
44
|
+
* @param {object} dependencies - The shared dependencies (e.g., logger).
|
|
45
|
+
* @returns {Promise<object>} The calculation result.
|
|
46
|
+
*/
|
|
47
|
+
async process(dateStr, rootData, dependencies) {
|
|
48
|
+
// { [sectorName]: { total_owners: 0 } }
|
|
49
|
+
const sectorOwners = new Map();
|
|
50
|
+
|
|
51
|
+
// 1. Load mappings
|
|
52
|
+
const mappings = await loadInstrumentMappings();
|
|
53
|
+
|
|
54
|
+
// 2. Get the insights document
|
|
55
|
+
const insightsDoc = rootData.insights;
|
|
56
|
+
if (!insightsDoc || !Array.isArray(insightsDoc.insights)) {
|
|
57
|
+
dependencies.logger.log('WARN', `[daily-ownership-per-sector] No 'insights' data found for ${dateStr}.`);
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 3. Iterate over the pre-aggregated array
|
|
62
|
+
for (const instrument of insightsDoc.insights) {
|
|
63
|
+
const instrumentId = instrument.instrumentId;
|
|
64
|
+
const totalOwners = instrument.total; // 'total' is the owner count
|
|
65
|
+
|
|
66
|
+
if (!instrumentId || typeof totalOwners !== 'number' || totalOwners === 0) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Find the sector for this instrument
|
|
71
|
+
const sectorName = mappings.instrumentToSectorName[instrumentId] || 'N/A';
|
|
72
|
+
|
|
73
|
+
// Initialize if new
|
|
74
|
+
if (!sectorOwners.has(sectorName)) {
|
|
75
|
+
sectorOwners.set(sectorName, { total_owners: 0 });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Add this instrument's owners to the sector's total
|
|
79
|
+
sectorOwners.get(sectorName).total_owners += totalOwners;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Convert Map to plain object for Firestore
|
|
83
|
+
return Object.fromEntries(sectorOwners);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = DailyOwnershipPerSector;
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Calculation (Pass 1) for total positions held.
|
|
3
3
|
*
|
|
4
|
-
* This is a
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* It provides a measure of overall market participation.
|
|
4
|
+
* REFACTOR: This is now a 'type: "meta"' calculation. It runs ONCE.
|
|
5
|
+
* It reads the pre-aggregated 'insights' data source and sums the
|
|
6
|
+
* 'total' field from all instruments to get the platform-wide total.
|
|
8
7
|
*/
|
|
9
8
|
class DailyTotalPositionsHeld {
|
|
10
|
-
|
|
11
|
-
this.totalPositions = 0;
|
|
12
|
-
}
|
|
13
|
-
|
|
9
|
+
|
|
14
10
|
/**
|
|
15
11
|
* Defines the output schema for this calculation.
|
|
16
12
|
* @returns {object} JSON Schema object
|
|
@@ -29,22 +25,37 @@ class DailyTotalPositionsHeld {
|
|
|
29
25
|
};
|
|
30
26
|
}
|
|
31
27
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
/**
|
|
29
|
+
* This is a 'meta' calculation. It runs once.
|
|
30
|
+
* @param {string} dateStr - The date string 'YYYY-MM-DD'.
|
|
31
|
+
* @param {object} rootData - The root data object. We expect rootData.insights.
|
|
32
|
+
* @param {object} dependencies - The shared dependencies (e.g., logger).
|
|
33
|
+
* @returns {object} The calculation result.
|
|
34
|
+
*/
|
|
35
|
+
process(dateStr, rootData, dependencies) {
|
|
36
|
+
let totalPositions = 0;
|
|
37
|
+
|
|
38
|
+
// rootData.insights contains the document from /daily_instrument_insights/
|
|
39
|
+
const insightsDoc = rootData.insights;
|
|
40
|
+
|
|
41
|
+
if (!insightsDoc || !Array.isArray(insightsDoc.insights)) {
|
|
42
|
+
dependencies.logger.log('WARN', `[daily-total-positions-held] No 'insights' data found for ${dateStr}.`);
|
|
43
|
+
return { totalPositions: 0 };
|
|
36
44
|
}
|
|
37
|
-
}
|
|
38
45
|
|
|
39
|
-
|
|
46
|
+
for (const instrument of insightsDoc.insights) {
|
|
47
|
+
// The 'total' field from the doc is the total # of positions for that instrument
|
|
48
|
+
if (typeof instrument.total === 'number') {
|
|
49
|
+
totalPositions += instrument.total;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
40
53
|
return {
|
|
41
|
-
totalPositions:
|
|
54
|
+
totalPositions: totalPositions
|
|
42
55
|
};
|
|
43
56
|
}
|
|
44
|
-
|
|
45
|
-
reset()
|
|
46
|
-
this.totalPositions = 0;
|
|
47
|
-
}
|
|
57
|
+
|
|
58
|
+
// No getResult() or reset() methods are needed for a 'meta' calculation
|
|
48
59
|
}
|
|
49
60
|
|
|
50
61
|
module.exports = DailyTotalPositionsHeld;
|
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Calculation (Pass
|
|
2
|
+
* @fileoverview Calculation (Pass 1) for daily ownership delta.
|
|
3
3
|
*
|
|
4
4
|
* This metric calculates the daily change in the total number of *owners*
|
|
5
5
|
* (unique users) for each instrument.
|
|
6
6
|
*
|
|
7
|
-
* This
|
|
7
|
+
* REFACTOR: This is now a 'type: "meta"' and 'isHistorical: true' calculation.
|
|
8
|
+
* It runs ONCE, loads today's and yesterday's pre-aggregated 'insights' docs,
|
|
9
|
+
* and calculates the delta based on the 'total' field.
|
|
8
10
|
*/
|
|
9
11
|
const { loadInstrumentMappings } = require('../../../utils/sector_mapping_provider');
|
|
10
12
|
|
|
11
13
|
class DailyOwnershipDelta {
|
|
12
|
-
|
|
13
|
-
// We will store { [instrumentId]: { owners_yesterday: Set(), owners_today: Set() } }
|
|
14
|
-
this.assetOwnership = new Map();
|
|
15
|
-
this.mappings = null;
|
|
16
|
-
}
|
|
17
|
-
|
|
14
|
+
|
|
18
15
|
/**
|
|
19
16
|
* Defines the output schema for this calculation.
|
|
20
17
|
* @returns {object} JSON Schema object
|
|
@@ -54,55 +51,49 @@ class DailyOwnershipDelta {
|
|
|
54
51
|
};
|
|
55
52
|
}
|
|
56
53
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const tIds = this._getInstrumentIds(todayPortfolio);
|
|
81
|
-
|
|
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
|
-
}
|
|
87
|
-
|
|
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);
|
|
54
|
+
/**
|
|
55
|
+
* This is a 'meta' and 'historical' calculation. It runs once.
|
|
56
|
+
* @param {string} dateStr - The date string 'YYYY-MM-DD'.
|
|
57
|
+
* @param {object} todayRootData - Root data for today. We expect todayRootData.insights.
|
|
58
|
+
* @param {object} yesterdayRootData - Root data for yesterday. We expect yesterdayRootData.insights.
|
|
59
|
+
* @returns {Promise<object>} The calculation result.
|
|
60
|
+
*/
|
|
61
|
+
async process(dateStr, todayRootData, yesterdayRootData) {
|
|
62
|
+
const mappings = await loadInstrumentMappings();
|
|
63
|
+
|
|
64
|
+
const todayOwners = new Map();
|
|
65
|
+
const yesterdayOwners = new Map();
|
|
66
|
+
const allInstrumentIds = new Set();
|
|
67
|
+
|
|
68
|
+
// 1. Process today's insights doc
|
|
69
|
+
const todayDoc = todayRootData.insights;
|
|
70
|
+
if (todayDoc && Array.isArray(todayDoc.insights)) {
|
|
71
|
+
for (const instrument of todayDoc.insights) {
|
|
72
|
+
const id = instrument.instrumentId;
|
|
73
|
+
const totalOwners = instrument.total || 0; // 'total' is the owner count
|
|
74
|
+
todayOwners.set(id, totalOwners);
|
|
75
|
+
allInstrumentIds.add(id);
|
|
76
|
+
}
|
|
92
77
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (
|
|
97
|
-
|
|
78
|
+
|
|
79
|
+
// 2. Process yesterday's insights doc
|
|
80
|
+
const yesterdayDoc = yesterdayRootData.insights;
|
|
81
|
+
if (yesterdayDoc && Array.isArray(yesterdayDoc.insights)) {
|
|
82
|
+
for (const instrument of yesterdayDoc.insights) {
|
|
83
|
+
const id = instrument.instrumentId;
|
|
84
|
+
const totalOwners = instrument.total || 0; // 'total' is the owner count
|
|
85
|
+
yesterdayOwners.set(id, totalOwners);
|
|
86
|
+
allInstrumentIds.add(id);
|
|
87
|
+
}
|
|
98
88
|
}
|
|
99
|
-
|
|
89
|
+
|
|
90
|
+
// 3. Calculate deltas
|
|
100
91
|
const result = {};
|
|
101
|
-
for (const
|
|
102
|
-
const ticker =
|
|
92
|
+
for (const instrumentId of allInstrumentIds) {
|
|
93
|
+
const ticker = mappings.instrumentToTicker[instrumentId] || `id_${instrumentId}`;
|
|
103
94
|
|
|
104
|
-
const
|
|
105
|
-
const
|
|
95
|
+
const tOwners = todayOwners.get(instrumentId) || 0;
|
|
96
|
+
const yOwners = yesterdayOwners.get(instrumentId) || 0;
|
|
106
97
|
|
|
107
98
|
if (yOwners > 0 || tOwners > 0) {
|
|
108
99
|
result[ticker] = {
|
|
@@ -113,13 +104,11 @@ class DailyOwnershipDelta {
|
|
|
113
104
|
};
|
|
114
105
|
}
|
|
115
106
|
}
|
|
107
|
+
|
|
116
108
|
return result;
|
|
117
109
|
}
|
|
118
110
|
|
|
119
|
-
reset()
|
|
120
|
-
this.assetOwnership.clear();
|
|
121
|
-
this.mappings = null;
|
|
122
|
-
}
|
|
111
|
+
// No getResult() or reset() methods are needed
|
|
123
112
|
}
|
|
124
113
|
|
|
125
114
|
module.exports = DailyOwnershipDelta;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Calculation (Pass 1) for P&L.
|
|
3
|
+
*
|
|
4
|
+
* This metric provides the *overall* platform-wide profitability ratio
|
|
5
|
+
* (total profitable positions / total unprofitable positions)
|
|
6
|
+
* across all users and all open positions.
|
|
7
|
+
*/
|
|
8
|
+
class ProfitabilityRatioOverall {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.profitable = 0;
|
|
11
|
+
this.unprofitable = 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Defines the output schema for this calculation.
|
|
16
|
+
* @returns {object} JSON Schema object
|
|
17
|
+
*/
|
|
18
|
+
static getSchema() {
|
|
19
|
+
return {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"description": "Calculates the overall profitability ratio (profitable/unprofitable positions) for the entire platform.",
|
|
22
|
+
"properties": {
|
|
23
|
+
"overall_ratio": {
|
|
24
|
+
"type": ["number", "null"],
|
|
25
|
+
"description": "Ratio of profitable to unprofitable positions (profitable / unprofitable). Null if 0 unprofitable."
|
|
26
|
+
},
|
|
27
|
+
"total_profitable": {
|
|
28
|
+
"type": "number",
|
|
29
|
+
"description": "Total count of all profitable positions."
|
|
30
|
+
},
|
|
31
|
+
"total_unprofitable": {
|
|
32
|
+
"type": "number",
|
|
33
|
+
"description": "Total count of all unprofitable positions."
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"required": ["overall_ratio", "total_profitable", "total_unprofitable"]
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
process(portfolioData) {
|
|
41
|
+
const positions = portfolioData.PublicPositions || portfolioData.AggregatedPositions;
|
|
42
|
+
if (!positions || !Array.isArray(positions)) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const pos of positions) {
|
|
47
|
+
const pnl = pos.NetProfit;
|
|
48
|
+
|
|
49
|
+
if (pnl > 0) {
|
|
50
|
+
this.profitable++;
|
|
51
|
+
} else if (pnl < 0) {
|
|
52
|
+
this.unprofitable++;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getResult() {
|
|
58
|
+
return {
|
|
59
|
+
overall_ratio: (this.unprofitable > 0) ? (this.profitable / this.unprofitable) : null,
|
|
60
|
+
total_profitable: this.profitable,
|
|
61
|
+
total_unprofitable: this.unprofitable
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
reset() {
|
|
66
|
+
this.profitable = 0;
|
|
67
|
+
this.unprofitable = 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = ProfitabilityRatioOverall;
|