bulltrackers-module 1.0.996 → 1.0.997
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/functions/computation-system-v3/framework/docs/authoring/sample-computations/grade1-portfolio-summary.js +78 -63
- package/functions/computation-system-v3/framework/docs/authoring/sample-computations/grade2-entity-risk-change.js +74 -103
- package/functions/computation-system-v3/framework/docs/authoring/sample-computations/grade3-alert-on-risk-spike.js +126 -66
- package/package.json +1 -1
|
@@ -1,93 +1,108 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Grade 1 sample computation —
|
|
2
|
+
* Grade 1 sample computation — basic per‑user portfolio snapshot using SDK helpers.
|
|
3
3
|
*
|
|
4
|
-
* This is
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
4
|
+
* This is intentionally the smallest realistic example of a V3 computation:
|
|
5
|
+
* - Minimal `config`: only `name`, `type`, and `storage`.
|
|
6
|
+
* - No manual `config.requires`: the engine infers it from the code using
|
|
7
|
+
* DataContracts (`__contracts`) attached to `lib.*` helpers.
|
|
8
|
+
* - Straight‑line `process(ctx)` that reads a single table and returns a
|
|
9
|
+
* small, envelope‑compatible result object.
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
+
* It is designed to be copied, modified, and extended by users.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Configuration object for the computation.
|
|
15
16
|
*
|
|
16
|
-
* - `name` is
|
|
17
|
-
* - `
|
|
18
|
-
*
|
|
19
|
-
* - `
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
17
|
+
* - `name` is unique per tenant and is used for scheduling and storage paths.
|
|
18
|
+
* - `type: 'per-entity'` means the computation runs once per entity ID
|
|
19
|
+
* (e.g. once per signed‑in user or PI, depending on the calling DAG).
|
|
20
|
+
* - `storage.bigquery: true` persists the result into the standard
|
|
21
|
+
* `computation_results_v3` dataset for this tenant.
|
|
22
|
+
*
|
|
23
|
+
* NOTE: We deliberately do NOT define `config.requires` here. The engine
|
|
24
|
+
* analyses the code in `process(ctx)` and infers a safe `requires` block
|
|
25
|
+
* from the `ctx.lib.portfolio.*` calls and `ctx.data.portfolio_snapshots`
|
|
26
|
+
* access (see `docs/authoring/config-resolution.md`).
|
|
23
27
|
*/
|
|
24
28
|
exports.config = {
|
|
25
|
-
name: '
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
requires: {
|
|
29
|
-
portfolio_snapshots: {
|
|
30
|
-
// The runtime will fetch up to 7 days of snapshots ending at ctx.date.
|
|
31
|
-
lookback: 7,
|
|
32
|
-
mandatory: true,
|
|
33
|
-
fields: ['date', 'total_value']
|
|
34
|
-
}
|
|
35
|
-
},
|
|
29
|
+
name: 'Sample_PortfolioSnapshot_Grade1',
|
|
30
|
+
type: 'per-entity',
|
|
31
|
+
category: 'examples',
|
|
36
32
|
storage: {
|
|
37
33
|
bigquery: true
|
|
38
34
|
}
|
|
39
35
|
};
|
|
40
36
|
|
|
41
37
|
/**
|
|
42
|
-
* Process function —
|
|
38
|
+
* Process function — summarises the latest portfolio snapshot for one entity.
|
|
43
39
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
40
|
+
* For the current `entityId` and `date`, this function:
|
|
41
|
+
* 1. Uses `lib.dataHelpers.getEntityRows` to get rows from
|
|
42
|
+
* `ctx.data.portfolio_snapshots` that belong to the current entity.
|
|
43
|
+
* 2. Sorts those rows by date ascending using `lib.dataHelpers.sortByDateAsc`.
|
|
44
|
+
* 3. Extracts the latest `portfolio_data` payload via `lib.portfolio.extractPortfolioData`.
|
|
45
|
+
* 4. Derives simple metrics using `lib.portfolio` helpers:
|
|
46
|
+
* - `calculateTotalInvested`
|
|
47
|
+
* - `calculateTotalValue`
|
|
48
|
+
* - `calculateWinRatio`
|
|
49
|
+
* 5. Returns a compact result object with no reserved keys (`status`, `error`).
|
|
49
50
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* 3. Compute a simple count and average total value over the window.
|
|
54
|
-
* 4. Log a short summary line with the number of snapshots analysed.
|
|
55
|
-
* 5. Return a result object with:
|
|
56
|
-
* - outcome: high-level status code for domain logic ("OK" or "NO_DATA").
|
|
57
|
-
* - asOfDate: the target date for the run.
|
|
58
|
-
* - snapshotCount: how many rows were actually used.
|
|
59
|
-
* - averagePortfolioValue: null if there was no valid data.
|
|
51
|
+
* This pattern matches how production computations in `../computations` use the SDK.
|
|
52
|
+
*
|
|
53
|
+
* @param {import('../core/ContextBuilder').ComputationContext} ctx
|
|
60
54
|
*/
|
|
61
55
|
exports.process = function process(ctx) {
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
56
|
+
const { data, entityId, date, lib, log } = ctx;
|
|
57
|
+
|
|
58
|
+
const rows = lib.dataHelpers.getEntityRows(data['portfolio_snapshots'], entityId);
|
|
59
|
+
const sorted = lib.dataHelpers.sortByDateAsc(rows);
|
|
60
|
+
const latest = sorted.length > 0 ? sorted[sorted.length - 1] : null;
|
|
65
61
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
62
|
+
if (!latest) {
|
|
63
|
+
if (log) {
|
|
64
|
+
log(
|
|
65
|
+
`[Sample_PortfolioSnapshot_Grade1] No portfolio_snapshots rows for entity ${String(
|
|
66
|
+
entityId
|
|
67
|
+
)} on ${date}`
|
|
68
|
+
);
|
|
72
69
|
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
outcome: 'NO_DATA',
|
|
73
|
+
asOfDate: date,
|
|
74
|
+
entityId: entityId ?? null,
|
|
75
|
+
totalInvested: 0,
|
|
76
|
+
totalValue: 0,
|
|
77
|
+
winRatioPercent: 0
|
|
78
|
+
};
|
|
73
79
|
}
|
|
74
80
|
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
const portfolioData = lib.portfolio.extractPortfolioData(latest);
|
|
82
|
+
const positions = lib.portfolio.extractPositions(portfolioData);
|
|
83
|
+
|
|
84
|
+
const totalInvested = lib.portfolio.calculateTotalInvested(positions);
|
|
85
|
+
const totalValue = lib.portfolio.calculateTotalValue(positions);
|
|
86
|
+
const winRatioPercent = lib.portfolio.calculateWinRatio(positions);
|
|
80
87
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
if (log) {
|
|
89
|
+
log(
|
|
90
|
+
`[Sample_PortfolioSnapshot_Grade1] entity=${String(
|
|
91
|
+
entityId
|
|
92
|
+
)} invested=${totalInvested.toFixed(2)} value=${totalValue.toFixed(
|
|
93
|
+
2
|
|
94
|
+
)} winRatio=${winRatioPercent.toFixed(2)}%`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
84
97
|
|
|
85
98
|
return {
|
|
86
|
-
//
|
|
87
|
-
outcome:
|
|
88
|
-
asOfDate:
|
|
89
|
-
|
|
90
|
-
|
|
99
|
+
// Domain‑level outcome; do not use reserved keys like `status` or `error`.
|
|
100
|
+
outcome: 'OK',
|
|
101
|
+
asOfDate: date,
|
|
102
|
+
entityId: entityId ?? null,
|
|
103
|
+
totalInvested: Number(totalInvested.toFixed(2)),
|
|
104
|
+
totalValue: Number(totalValue.toFixed(2)),
|
|
105
|
+
winRatioPercent: Number(winRatioPercent.toFixed(2))
|
|
91
106
|
};
|
|
92
107
|
};
|
|
93
108
|
|
|
@@ -1,145 +1,116 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Grade 2 sample computation —
|
|
2
|
+
* Grade 2 sample computation — PI risk and AUM summary over a short window.
|
|
3
3
|
*
|
|
4
|
-
* This example builds on the
|
|
5
|
-
*
|
|
6
|
-
* - Working with two logical inputs (`risk_snapshots` and `portfolio_snapshots`).
|
|
7
|
-
* - Computing a small derived metric (risk delta) per entity.
|
|
8
|
-
* - Demonstrating safe handling of missing / partial data.
|
|
4
|
+
* This example builds directly on the real `pi_rankings` table and uses the
|
|
5
|
+
* `lib.rankings` and `lib.dataHelpers` modules exactly as production recipes do.
|
|
9
6
|
*
|
|
10
|
-
* It
|
|
11
|
-
*
|
|
7
|
+
* It demonstrates:
|
|
8
|
+
* - Per‑entity operation (`type: 'per-entity'` for PI entities).
|
|
9
|
+
* - A small but explicit `config.requires` override to control lookback,
|
|
10
|
+
* fields, and mandatory flags.
|
|
11
|
+
* - Safe handling of missing data across days.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Configuration object for the computation.
|
|
16
16
|
*
|
|
17
|
-
* - `name`
|
|
18
|
-
* - `
|
|
19
|
-
*
|
|
20
|
-
* - `storage.bigquery: true` persists
|
|
17
|
+
* - `name` and `type` follow the same pattern as `RiskScoreIncrease`.
|
|
18
|
+
* - `requires.pi_rankings` is kept intentionally small but explicit so the
|
|
19
|
+
* author can see and tune lookback/fields in one place.
|
|
20
|
+
* - `storage.bigquery: true` persists results for later dashboard use.
|
|
21
21
|
*/
|
|
22
22
|
exports.config = {
|
|
23
|
-
name: '
|
|
23
|
+
name: 'Sample_PIRiskAndAum_Grade2',
|
|
24
24
|
type: 'per-entity',
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
category: 'examples',
|
|
26
|
+
|
|
27
27
|
requires: {
|
|
28
|
-
|
|
29
|
-
//
|
|
30
|
-
lookback:
|
|
31
|
-
mandatory: true,
|
|
32
|
-
fields: ['date', 'entity_id', 'risk_score']
|
|
33
|
-
},
|
|
34
|
-
portfolio_snapshots: {
|
|
35
|
-
// Optional: we only need a name/label for display purposes.
|
|
36
|
-
lookback: 1,
|
|
28
|
+
pi_rankings: {
|
|
29
|
+
// 7‑day window of rankings for this PI, similar to DashboardPage.
|
|
30
|
+
lookback: 7,
|
|
37
31
|
mandatory: false,
|
|
38
|
-
fields: ['
|
|
32
|
+
fields: ['pi_id', 'rankings_data', 'username', 'date']
|
|
39
33
|
}
|
|
40
34
|
},
|
|
35
|
+
|
|
41
36
|
storage: {
|
|
42
37
|
bigquery: true
|
|
43
38
|
}
|
|
44
39
|
};
|
|
45
40
|
|
|
46
41
|
/**
|
|
47
|
-
* Process function —
|
|
42
|
+
* Process function — summarises basic risk and AUM information for a single PI.
|
|
43
|
+
*
|
|
44
|
+
* For each PI (`entityId`) this function:
|
|
45
|
+
* 1. Pulls that PI's `pi_rankings` rows via `lib.dataHelpers.getEntityRows`.
|
|
46
|
+
* 2. Sorts the rows by date, then picks the latest one in the lookback window.
|
|
47
|
+
* 3. Uses `lib.rankings` helpers to extract:
|
|
48
|
+
* - current risk score and risk tier,
|
|
49
|
+
* - total gain,
|
|
50
|
+
* - AUM value and tier label.
|
|
51
|
+
* 4. Returns a compact result object, suitable for direct consumption by
|
|
52
|
+
* dashboards or downstream computations.
|
|
48
53
|
*
|
|
49
|
-
*
|
|
50
|
-
* 1. Filter the `risk_snapshots` array down to the current entityId (if provided).
|
|
51
|
-
* 2. Sort snapshots by date to find the "previous" and "current" entries.
|
|
52
|
-
* 3. Compute a simple delta: current_risk - previous_risk.
|
|
53
|
-
* 4. Look up a human-readable portfolio name from `portfolio_snapshots` if present.
|
|
54
|
-
* 5. Return a single object that describes today's risk position for this entity.
|
|
54
|
+
* @param {import('../core/ContextBuilder').ComputationContext} ctx
|
|
55
55
|
*/
|
|
56
56
|
exports.process = function process(ctx) {
|
|
57
|
-
const entityId = ctx
|
|
57
|
+
const { data, entityId, date, lib, log } = ctx;
|
|
58
58
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
const rows = lib.dataHelpers.getEntityRows(data['pi_rankings'], entityId);
|
|
60
|
+
const sorted = lib.dataHelpers.sortByDateAsc(rows);
|
|
61
|
+
const latest = sorted.length > 0 ? sorted[sorted.length - 1] : null;
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
entityRiskRows.sort((a, b) => {
|
|
72
|
-
const ad = a && a.date ? String(a.date) : '';
|
|
73
|
-
const bd = b && b.date ? String(b.date) : '';
|
|
74
|
-
return ad.localeCompare(bd);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const rowCount = entityRiskRows.length;
|
|
78
|
-
if (rowCount === 0) {
|
|
79
|
-
ctx.log(
|
|
80
|
-
`[Sample_EntityRiskChange_Grade2] no risk_snapshots found for entity ${String(
|
|
81
|
-
entityId || 'GLOBAL'
|
|
82
|
-
)} on ${ctx.date}`
|
|
83
|
-
);
|
|
63
|
+
if (!latest) {
|
|
64
|
+
if (log) {
|
|
65
|
+
log(
|
|
66
|
+
`[Sample_PIRiskAndAum_Grade2] No pi_rankings rows for entity ${String(
|
|
67
|
+
entityId
|
|
68
|
+
)} in window ending ${date}`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
84
71
|
|
|
85
72
|
return {
|
|
86
73
|
outcome: 'NO_DATA',
|
|
87
|
-
asOfDate:
|
|
88
|
-
entityId: entityId
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
74
|
+
asOfDate: date,
|
|
75
|
+
entityId: entityId ?? null,
|
|
76
|
+
username: null,
|
|
77
|
+
riskScore: null,
|
|
78
|
+
riskTier: null,
|
|
79
|
+
totalGain: null,
|
|
80
|
+
aumValue: null,
|
|
81
|
+
aumTierLabel: null
|
|
92
82
|
};
|
|
93
83
|
}
|
|
94
84
|
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
const currentRisk =
|
|
99
|
-
latest && typeof latest.risk_score === 'number'
|
|
100
|
-
? latest.risk_score
|
|
101
|
-
: Number(latest && latest.risk_score);
|
|
102
|
-
const previousRisk =
|
|
103
|
-
previous && typeof previous.risk_score === 'number'
|
|
104
|
-
? previous.risk_score
|
|
105
|
-
: Number(previous && previous.risk_score);
|
|
85
|
+
const rankingsData = lib.rankings.extractRankingsData(latest);
|
|
86
|
+
const username = lib.rankings.getUsername(latest);
|
|
106
87
|
|
|
107
|
-
const
|
|
108
|
-
const
|
|
88
|
+
const riskScore = lib.rankings.getRiskScore(rankingsData);
|
|
89
|
+
const riskTier = lib.rankings.getRiskTier(riskScore);
|
|
109
90
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
91
|
+
const totalGain = lib.rankings.getTotalGain(rankingsData);
|
|
92
|
+
const aumValue = lib.rankings.getAUM(rankingsData);
|
|
93
|
+
const aumTierId = lib.rankings.getAUMTier(rankingsData);
|
|
94
|
+
const aumTierLabel = lib.rankings.getAUMTierLabel(aumTierId);
|
|
114
95
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (!row) return false;
|
|
122
|
-
if (entityId == null) return false;
|
|
123
|
-
return String(row.entity_id) === String(entityId);
|
|
124
|
-
});
|
|
125
|
-
if (matchingPortfolio && typeof matchingPortfolio.display_name === 'string') {
|
|
126
|
-
portfolioName = matchingPortfolio.display_name;
|
|
96
|
+
if (log) {
|
|
97
|
+
log(
|
|
98
|
+
`[Sample_PIRiskAndAum_Grade2] entity=${String(
|
|
99
|
+
entityId
|
|
100
|
+
)} risk=${riskScore} (${riskTier}), AUM=${aumValue}, gain=${totalGain}`
|
|
101
|
+
);
|
|
127
102
|
}
|
|
128
103
|
|
|
129
|
-
ctx.log(
|
|
130
|
-
`[Sample_EntityRiskChange_Grade2] entity ${String(
|
|
131
|
-
entityId || 'GLOBAL'
|
|
132
|
-
)} risk=${safeCurrent} (delta=${riskDelta}) on ${ctx.date}`
|
|
133
|
-
);
|
|
134
|
-
|
|
135
104
|
return {
|
|
136
105
|
outcome: 'OK',
|
|
137
|
-
asOfDate:
|
|
138
|
-
entityId: entityId
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
106
|
+
asOfDate: date,
|
|
107
|
+
entityId: entityId ?? null,
|
|
108
|
+
username: username || null,
|
|
109
|
+
riskScore,
|
|
110
|
+
riskTier,
|
|
111
|
+
totalGain,
|
|
112
|
+
aumValue,
|
|
113
|
+
aumTierLabel
|
|
143
114
|
};
|
|
144
115
|
};
|
|
145
116
|
|
|
@@ -1,102 +1,162 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Grade 3 sample computation —
|
|
2
|
+
* Grade 3 sample computation — asset + PI risk alerts (composed from real tables).
|
|
3
3
|
*
|
|
4
|
-
* This example
|
|
5
|
-
*
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* This example shows an end‑to‑end, production‑style pattern:
|
|
5
|
+
* - Depends on the existing `GlobalAumPerAsset30D` computation so it runs first.
|
|
6
|
+
* - Reads its materialised results from the `results` table via data helpers.
|
|
7
|
+
* - Combines that with live `pi_rankings` data to build two alert feeds:
|
|
8
|
+
* • high‑AUM assets
|
|
9
|
+
* • high‑risk, high‑AUM Popular Investors
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* The intent is to be a “perfect role model”: everything is grounded in the
|
|
12
|
+
* current framework, no invented tables, and every step is documented.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Configuration
|
|
16
|
+
* Configuration for the computation.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* Key points:
|
|
19
|
+
* - `type: 'global'` because we aggregate across all entities.
|
|
20
|
+
* - `dependencies` ensures `GlobalAumPerAsset30D` has written its output to
|
|
21
|
+
* the `results` table before this computation runs.
|
|
22
|
+
* - `requires.results` explicitly filters that table to only the parent
|
|
23
|
+
* computation’s rows, mirroring `GlobalAumPerAsset30D`.
|
|
24
|
+
* - `requires.pi_rankings` gives us current PI risk/AUM data for scoring.
|
|
22
25
|
*/
|
|
23
26
|
exports.config = {
|
|
24
|
-
name: '
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
name: 'Sample_AssetAndRiskAlerts_Grade3',
|
|
28
|
+
type: 'global',
|
|
29
|
+
category: 'examples',
|
|
30
|
+
|
|
31
|
+
// Ensure the upstream aggregation has run before this computation executes.
|
|
32
|
+
dependencies: ['globalaumperasset30d'],
|
|
33
|
+
|
|
28
34
|
requires: {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
lookback:
|
|
35
|
+
// Parent computation results, constrained to GlobalAumPerAsset30D.
|
|
36
|
+
results: {
|
|
37
|
+
lookback: 0,
|
|
32
38
|
mandatory: true,
|
|
33
|
-
fields: [
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
]
|
|
39
|
+
fields: ['result_data', 'computation_name', 'date', 'entity_id'],
|
|
40
|
+
filter: { computation_name: 'globalaumperasset30d' }
|
|
41
|
+
},
|
|
42
|
+
// Current PI rankings window; used to identify high‑risk, high‑AUM PIs.
|
|
43
|
+
pi_rankings: {
|
|
44
|
+
lookback: 7,
|
|
45
|
+
mandatory: false,
|
|
46
|
+
fields: ['pi_id', 'rankings_data', 'username', 'date']
|
|
41
47
|
}
|
|
42
48
|
},
|
|
49
|
+
|
|
43
50
|
storage: {
|
|
44
51
|
bigquery: true
|
|
45
52
|
}
|
|
46
53
|
};
|
|
47
54
|
|
|
48
55
|
/**
|
|
49
|
-
* Process function —
|
|
56
|
+
* Process function — produces alert lists for assets and PIs.
|
|
57
|
+
*
|
|
58
|
+
* High‑level behaviour:
|
|
59
|
+
*
|
|
60
|
+
* 1. Reads `results` for `GlobalAumPerAsset30D` using:
|
|
61
|
+
* - `lib.dataHelpers.normaliseParentResults` to flatten rows
|
|
62
|
+
* - `lib.dataHelpers.parseResultData` to parse `result_data`
|
|
63
|
+
* and emits **asset AUM alerts** where total AUM exceeds a fixed threshold.
|
|
50
64
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* 3. Map each row to a small, explicit alert object.
|
|
55
|
-
* 4. Return a payload containing:
|
|
56
|
-
* - outcome: "OK" or "NO_ALERTS".
|
|
57
|
-
* - asOfDate: the date we evaluated.
|
|
58
|
-
* - alerts: array of alert objects (possibly empty).
|
|
65
|
+
* 2. Reads `pi_rankings` via `lib.dataHelpers.toArray` and `lib.rankings`
|
|
66
|
+
* helpers to construct **PI risk alerts** for Popular Investors that are
|
|
67
|
+
* both high‑risk and above a minimum AUM tier.
|
|
59
68
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
69
|
+
* 3. Returns a single, envelope‑compatible result object:
|
|
70
|
+
* - `outcome`: `"OK"` or `"NO_ALERTS"`
|
|
71
|
+
* - `asOfDate`: target date
|
|
72
|
+
* - `assetAlerts`: array of asset‑level alerts
|
|
73
|
+
* - `piRiskAlerts`: array of PI‑level alerts
|
|
74
|
+
*
|
|
75
|
+
* @param {import('../core/ContextBuilder').ComputationContext} ctx
|
|
62
76
|
*/
|
|
63
77
|
exports.process = function process(ctx) {
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
78
|
+
const { data, date, lib, log } = ctx;
|
|
79
|
+
|
|
80
|
+
const { normaliseParentResults, parseResultData, toArray } = lib.dataHelpers;
|
|
81
|
+
|
|
82
|
+
// -------------------------------------------------------------------------
|
|
83
|
+
// 1. Asset‑level alerts from GlobalAumPerAsset30D parent results
|
|
84
|
+
// -------------------------------------------------------------------------
|
|
85
|
+
const parentRows = normaliseParentResults(data['results']);
|
|
86
|
+
const assetAlerts = [];
|
|
87
|
+
|
|
88
|
+
// Example threshold: only alert on assets with at least 50k AUM.
|
|
89
|
+
const ASSET_AUM_THRESHOLD = 50000;
|
|
67
90
|
|
|
68
|
-
const
|
|
91
|
+
for (const row of parentRows) {
|
|
92
|
+
const parsed = parseResultData(row) || row;
|
|
69
93
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
94
|
+
// GlobalAumPerAsset30D returns an array of { symbol, amount } entries
|
|
95
|
+
// as its top‑level `result`. We tolerate both raw arrays and envelopes.
|
|
96
|
+
const items = Array.isArray(parsed)
|
|
97
|
+
? parsed
|
|
98
|
+
: (Array.isArray(parsed.result) ? parsed.result : []);
|
|
73
99
|
|
|
74
|
-
const
|
|
75
|
-
|
|
100
|
+
for (const item of items) {
|
|
101
|
+
if (!item || item.symbol == null || item.amount == null) continue;
|
|
102
|
+
const amount = Number(item.amount);
|
|
103
|
+
if (!Number.isFinite(amount) || amount < ASSET_AUM_THRESHOLD) continue;
|
|
76
104
|
|
|
77
|
-
|
|
78
|
-
|
|
105
|
+
assetAlerts.push({
|
|
106
|
+
type: 'ASSET_AUM',
|
|
107
|
+
symbol: item.symbol,
|
|
108
|
+
amount: Number(amount.toFixed(2)),
|
|
109
|
+
asOfDate: date
|
|
110
|
+
});
|
|
79
111
|
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// -------------------------------------------------------------------------
|
|
115
|
+
// 2. PI‑level risk alerts from pi_rankings
|
|
116
|
+
// -------------------------------------------------------------------------
|
|
117
|
+
const rankings = toArray(data['pi_rankings'] || []);
|
|
118
|
+
const piRiskAlerts = [];
|
|
119
|
+
|
|
120
|
+
// Example policy: alert on "high" or above risk and mid‑tier AUM or higher.
|
|
121
|
+
const MIN_RISK_SCORE = 6;
|
|
122
|
+
const MIN_AUM_TIER = 3;
|
|
123
|
+
|
|
124
|
+
rankings.forEach((row) => {
|
|
125
|
+
const rData = lib.rankings.extractRankingsData(row);
|
|
126
|
+
if (!rData) return;
|
|
127
|
+
|
|
128
|
+
const riskScore = lib.rankings.getRiskScore(rData);
|
|
129
|
+
const riskTier = lib.rankings.getRiskTier(riskScore);
|
|
130
|
+
const aumValue = lib.rankings.getAUM(rData);
|
|
131
|
+
const aumTierId = lib.rankings.getAUMTier(rData);
|
|
80
132
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
133
|
+
if (riskScore < MIN_RISK_SCORE || aumTierId < MIN_AUM_TIER) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
piRiskAlerts.push({
|
|
138
|
+
type: 'PI_RISK',
|
|
139
|
+
piId: lib.rankings.getPiId(row),
|
|
140
|
+
username: lib.rankings.getUsername(row),
|
|
141
|
+
riskScore,
|
|
142
|
+
riskTier,
|
|
143
|
+
aumValue,
|
|
144
|
+
aumTierLabel: lib.rankings.getAUMTierLabel(aumTierId)
|
|
89
145
|
});
|
|
90
|
-
}
|
|
146
|
+
});
|
|
91
147
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
148
|
+
if (log) {
|
|
149
|
+
log(
|
|
150
|
+
`[Sample_AssetAndRiskAlerts_Grade3] generated ${assetAlerts.length} asset alert(s) and ${piRiskAlerts.length} PI risk alert(s) for ${date}`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
95
153
|
|
|
96
154
|
return {
|
|
97
|
-
outcome
|
|
98
|
-
|
|
99
|
-
|
|
155
|
+
// Use a domain‑level outcome; never return reserved keys `status` or `error`.
|
|
156
|
+
outcome: assetAlerts.length === 0 && piRiskAlerts.length === 0 ? 'NO_ALERTS' : 'OK',
|
|
157
|
+
asOfDate: date,
|
|
158
|
+
assetAlerts,
|
|
159
|
+
piRiskAlerts
|
|
100
160
|
};
|
|
101
161
|
};
|
|
102
162
|
|