aiden-shared-calculations-unified 1.0.109 → 1.0.110

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.
@@ -1,142 +1,66 @@
1
- /**
2
- * @fileoverview GEM Product Line (Pass 2)
3
- * REFACTORED: Uses context.computed and context.math.extract.
4
- */
5
1
  class UnskilledCohortFlow {
6
- constructor() {
7
- this.assetFlows = new Map();
8
- this.cohortMap = new Map();
9
- this.dependenciesLoaded = false;
10
- this.tickerMap = null;
11
- }
12
-
13
- static getMetadata() {
14
- return {
15
- type: 'standard',
16
- rootDataDependencies: ['portfolio'],
17
- isHistorical: true,
18
- userType: 'all',
19
- category: 'gem'
20
- };
21
- }
22
-
23
- static getDependencies() {
24
- return ['cohort-skill-definition', 'instrument-price-change-1d'];
25
- }
2
+ constructor() { this.assetFlows = new Map(); this.cohortMap = new Map(); this.dependenciesLoaded = false; this.tickerMap = null; }
3
+ static getMetadata() { return { type: 'standard', rootDataDependencies: ['portfolio'], isHistorical: true, userType: 'all', category: 'gem' }; }
4
+ static getDependencies() { return ['cohort-skill-definition', 'instrument-price-change-1d']; }
26
5
 
27
6
  static getSchema() {
28
- const tickerSchema = {
29
- "type": "object",
30
- "properties": {
31
- "net_flow_pct": { "type": "number" },
32
- "net_flow_contribution": { "type": "number" },
33
- "avg_position_change_pct": { "type": "number" },
34
- "user_count": { "type": "number" }
35
- },
36
- "required": ["net_flow_pct", "net_flow_contribution", "avg_position_change_pct", "user_count"]
37
- };
38
- return { "type": "object", "patternProperties": { "^.*$": tickerSchema } };
39
- }
40
-
41
- _initFlowData(instrumentId) {
42
- if (!this.assetFlows.has(instrumentId)) {
43
- this.assetFlows.set(instrumentId, {
44
- total_invested_yesterday: 0, total_invested_today: 0, price_change_yesterday: 0,
45
- total_pos_size_yesterday: 0, total_pos_size_today: 0, user_count_yesterday: 0, user_count_today: 0
46
- });
47
- }
7
+ const s = { "type": "object", "properties": { "net_flow_pct": { "type": ["number", "null"] }, "net_flow_contribution": { "type": ["number", "null"] }, "avg_position_change_pct": { "type": ["number", "null"] }, "user_count": { "type": "number" }, "total_invested_today": { "type": "number" } }, "required": ["net_flow_pct", "net_flow_contribution", "avg_position_change_pct", "user_count", "total_invested_today"] };
8
+ return { "type": "object", "patternProperties": { "^.*$": s } };
48
9
  }
49
10
 
50
11
  _loadDependencies(computed) {
51
12
  if (this.dependenciesLoaded) return;
52
- const cohortData = computed['cohort-skill-definition'];
53
- if (cohortData) {
54
- (cohortData.unskilled_user_ids || []).forEach(uid => this.cohortMap.set(String(uid), 'unskilled'));
55
- }
13
+ const d = computed['cohort-skill-definition'];
14
+ if (d) (d.unskilled_user_ids || []).forEach(uid => this.cohortMap.set(String(uid), 'unskilled'));
56
15
  this.dependenciesLoaded = true;
57
16
  }
58
17
 
59
18
  process(context) {
60
19
  const { user, computed, mappings, math } = context;
61
20
  const { extract } = math;
62
-
63
21
  this._loadDependencies(computed);
64
- if (!this.tickerMap) this.tickerMap = mappings.instrumentToTicker;
65
-
66
22
  if (this.cohortMap.get(user.id) !== 'unskilled') return;
67
-
68
- const priceChangeMap = computed['instrument-price-change-1d'];
69
- if (!priceChangeMap) return;
23
+ if (!this.tickerMap) this.tickerMap = mappings.instrumentToTicker;
24
+ const priceMap = computed['instrument-price-change-1d'];
70
25
 
71
- const yPos = extract.getPositions(user.portfolio.yesterday, user.type);
72
26
  const tPos = extract.getPositions(user.portfolio.today, user.type);
73
-
27
+ const yPos = user.portfolio.yesterday ? extract.getPositions(user.portfolio.yesterday, user.type) : [];
74
28
  const yPosMap = new Map(yPos.map(p => [extract.getInstrumentId(p), p]));
75
29
  const tPosMap = new Map(tPos.map(p => [extract.getInstrumentId(p), p]));
76
- const allInstrumentIds = new Set([...yPosMap.keys(), ...tPosMap.keys()]);
77
-
78
- for (const instId of allInstrumentIds) {
79
- if (!instId) continue;
80
- this._initFlowData(instId);
81
- const asset = this.assetFlows.get(instId);
82
-
83
- const yWeight = extract.getPositionWeight(yPosMap.get(instId), user.type);
84
- const tWeight = extract.getPositionWeight(tPosMap.get(instId), user.type);
30
+ const ids = new Set([...yPosMap.keys(), ...tPosMap.keys()]);
85
31
 
86
- if (yWeight > 0) {
87
- asset.total_invested_yesterday += yWeight;
88
- const ticker = mappings.instrumentToTicker[instId];
89
- const priceChange = (ticker && priceChangeMap[ticker]) ? priceChangeMap[ticker].change_1d_pct : 0;
90
- asset.price_change_yesterday += (priceChange / 100.0) * yWeight;
91
- asset.total_pos_size_yesterday += yWeight;
92
- asset.user_count_yesterday++;
93
- }
94
- if (tWeight > 0) {
95
- asset.total_invested_today += tWeight;
96
- asset.user_count_today++;
97
- asset.total_pos_size_today += tWeight;
32
+ for (const id of ids) {
33
+ if (!this.assetFlows.has(id)) this.assetFlows.set(id, { yInv: 0, tInv: 0, pChg: 0, ySize: 0, tSize: 0, yCount: 0, tCount: 0, hasY: false });
34
+ const asset = this.assetFlows.get(id);
35
+ const tW = extract.getPositionWeight(tPosMap.get(id), user.type);
36
+ if (tW > 0) { asset.tInv += tW; asset.tCount++; asset.tSize += tW; }
37
+ if (user.portfolio.yesterday) {
38
+ asset.hasY = true;
39
+ const yW = extract.getPositionWeight(yPosMap.get(id), user.type);
40
+ if (yW > 0) {
41
+ asset.yInv += yW; asset.yCount++; asset.ySize += yW;
42
+ const tick = mappings.instrumentToTicker[id];
43
+ const chg = (tick && priceMap?.[tick]) ? priceMap[tick].change_1d_pct : 0;
44
+ asset.pChg += ((chg || 0) / 100.0) * yW;
45
+ }
98
46
  }
99
47
  }
100
48
  }
101
49
 
102
50
  async getResult() {
103
- if (!this.tickerMap) return {};
104
- const finalResult = {};
105
- for (const [instId, data] of this.assetFlows.entries()) {
106
- const ticker = this.tickerMap[instId];
107
- if (!ticker) continue;
108
- let net_flow = 0, flow_contrib = 0;
109
- if (data.total_invested_yesterday > 0) {
110
- const avg_change = data.price_change_yesterday / data.total_invested_yesterday;
111
- const adjusted_y = data.total_invested_yesterday * (1 + avg_change);
112
- flow_contrib = data.total_invested_today - adjusted_y;
113
- net_flow = (flow_contrib / data.total_invested_yesterday) * 100;
114
- } else if (data.total_invested_today > 0) {
115
- flow_contrib = data.total_invested_today;
116
- net_flow = Infinity;
51
+ if (!this.tickerMap) return {}; const res = {};
52
+ for (const [id, d] of this.assetFlows.entries()) {
53
+ const tick = this.tickerMap[id]; if (!tick) continue;
54
+ let net = null, contrib = null, avgPos = null;
55
+ if (d.hasY) {
56
+ const adjY = d.yInv + d.pChg; contrib = d.tInv - adjY;
57
+ net = d.yInv > 0 ? (contrib / d.yInv) * 100 : (d.tInv > 0 ? Infinity : 0);
58
+ if (d.yCount > 0 && d.tCount > 0) avgPos = (((d.tSize / d.tCount) - (d.ySize / d.yCount)) / (d.ySize / d.yCount)) * 100;
117
59
  }
118
- let avg_pos_change = 0;
119
- if (data.user_count_yesterday > 0 && data.user_count_today > 0) {
120
- const avg_y = data.total_pos_size_yesterday / data.user_count_yesterday;
121
- const avg_t = data.total_pos_size_today / data.user_count_today;
122
- if (avg_y > 0) avg_pos_change = ((avg_t - avg_y) / avg_y) * 100;
123
- } else if (data.user_count_today > 0) avg_pos_change = Infinity;
124
-
125
- finalResult[ticker] = {
126
- net_flow_pct: isFinite(net_flow) ? net_flow : 0,
127
- net_flow_contribution: isFinite(flow_contrib) ? flow_contrib : 0,
128
- avg_position_change_pct: isFinite(avg_pos_change) ? avg_pos_change : 0,
129
- user_count: data.user_count_today
130
- };
60
+ res[tick] = { net_flow_pct: isFinite(net) ? net : null, net_flow_contribution: isFinite(contrib) ? contrib : null, avg_position_change_pct: isFinite(avgPos) ? avgPos : null, user_count: d.tCount, total_invested_today: d.tInv };
131
61
  }
132
- return finalResult;
133
- }
134
-
135
- reset() {
136
- this.assetFlows.clear();
137
- this.cohortMap.clear();
138
- this.dependenciesLoaded = false;
139
- this.tickerMap = null;
62
+ return res;
140
63
  }
64
+ reset() { this.assetFlows.clear(); this.cohortMap.clear(); this.dependenciesLoaded = false; this.tickerMap = null; }
141
65
  }
142
66
  module.exports = UnskilledCohortFlow;
@@ -1,80 +1,33 @@
1
- /**
2
- * @fileoverview Ghost Book: Retail Gamma Exposure
3
- * Measures the "Elasticity of Intent" (Beta) between Price Change and Net Flow.
4
- */
5
1
  class RetailGammaExposure {
6
2
  constructor() { this.gammaResults = {}; }
7
-
8
- static getMetadata() {
9
- return {
10
- type: 'meta',
11
- dependencies: ['skilled-cohort-flow', 'instrument-price-change-1d'],
12
- isHistorical: true, // Requires t-1, t-2... for rolling regression
13
- userType: 'n/a', // FIXED: Added missing field
14
- category: 'ghost_book'
15
- };
16
- }
17
-
3
+ static getMetadata() { return { type: 'meta', dependencies: ['skilled-cohort-flow', 'instrument-price-change-1d'], isHistorical: true, userType: 'n/a', category: 'ghost_book' }; }
18
4
  static getDependencies() { return ['skilled-cohort-flow', 'instrument-price-change-1d']; }
19
5
 
20
6
  static getSchema() {
21
- return {
22
- "type": "object",
23
- "patternProperties": {
24
- "^.*$": {
25
- "type": "object",
26
- "properties": {
27
- "gamma_beta": { "type": "number" },
28
- "regime": { "type": "string", "enum": ["ACCELERANT", "STABILIZER", "NEUTRAL"] },
29
- "_state": { "type": "object" } // Rolling buffer
30
- }
31
- }
32
- }
33
- };
7
+ return { "type": "object", "patternProperties": { "^.*$": { "type": "object", "properties": { "gamma_beta": { "type": ["number", "null"] }, "regime": { "type": "string" }, "_state": { "type": "object" } } } } };
34
8
  }
35
9
 
36
10
  process(context) {
37
11
  const { computed, previousComputed, math } = context;
38
12
  const { signals, distribution } = math;
39
-
40
13
  const tickers = signals.getUnionKeys(computed, ['skilled-cohort-flow']);
41
-
42
14
  for (const ticker of tickers) {
43
15
  const flow = signals.getMetric(computed, 'skilled-cohort-flow', ticker, 'net_flow_pct', 0);
44
- const priceChange = signals.getMetric(computed, 'instrument-price-change-1d', ticker, 'change_1d_pct', 0);
45
-
46
- const prevResult = signals.getPreviousState(previousComputed, 'retail-gamma-exposure', ticker);
47
- const bufferSize = 14;
48
-
49
- let flowBuffer = prevResult?._state?.flow_buffer || [];
50
- let priceBuffer = prevResult?._state?.price_buffer || [];
51
-
52
- flowBuffer.push(flow);
53
- priceBuffer.push(priceChange);
54
-
55
- if (flowBuffer.length > bufferSize) {
56
- flowBuffer.shift();
57
- priceBuffer.shift();
16
+ const price = signals.getMetric(computed, 'instrument-price-change-1d', ticker, 'change_1d_pct', 0);
17
+ const prev = signals.getPreviousState(previousComputed, 'retail-gamma-exposure', ticker);
18
+ let fBuf = [...(prev?._state?.flow_buffer || [])], pBuf = [...(prev?._state?.price_buffer || [])];
19
+ fBuf.push(flow); pBuf.push(price);
20
+ if (fBuf.length > 14) { fBuf.shift(); pBuf.shift(); }
21
+
22
+ let beta = null, regime = "WARM_UP";
23
+ if (fBuf.length >= 7) {
24
+ const reg = distribution.linearRegression(pBuf, fBuf);
25
+ beta = reg.slope;
26
+ regime = beta > 0.5 ? "ACCELERANT" : (beta < -0.5 ? "STABILIZER" : "NEUTRAL");
58
27
  }
59
-
60
- const regression = distribution.linearRegression(priceBuffer, flowBuffer);
61
- const beta = regression.slope;
62
-
63
- let regime = "NEUTRAL";
64
- if (beta > 0.5) regime = "ACCELERANT";
65
- else if (beta < -0.5) regime = "STABILIZER";
66
-
67
- this.gammaResults[ticker] = {
68
- gamma_beta: Number(beta.toFixed(4)),
69
- regime: regime,
70
- _state: {
71
- flow_buffer: flowBuffer,
72
- price_buffer: priceBuffer
73
- }
74
- };
28
+ this.gammaResults[ticker] = { gamma_beta: (beta !== null && isFinite(beta)) ? Number(beta.toFixed(4)) : null, regime, _state: { flow_buffer: fBuf, price_buffer: pBuf } };
75
29
  }
76
30
  }
77
-
78
31
  getResult() { return this.gammaResults; }
79
32
  reset() { this.gammaResults = {}; }
80
33
  }
@@ -1,108 +1,45 @@
1
- /**
2
- * @fileoverview HELIX Product Line (Pass 2)
3
- * REFACTORED: Uses context.math.extract.
4
- */
5
1
  class HerdConsensusScore {
6
- constructor() {
7
- this.assetConviction = new Map();
8
- this.tickerMap = null;
9
- }
2
+ constructor() { this.assetConviction = new Map(); this.tickerMap = null; }
3
+ static getMetadata() { return { type: 'standard', rootDataDependencies: ['portfolio'], isHistorical: true, userType: 'all', category: 'helix' }; }
4
+ static getDependencies() { return []; }
10
5
 
11
6
  static getSchema() {
12
- const tickerSchema = {
13
- "type": "object",
14
- "properties": {
15
- "herd_conviction_score": { "type": "number" },
16
- "user_count": { "type": "number" }
17
- },
18
- "required": ["herd_conviction_score", "user_count"]
19
- };
20
- return { "type": "object", "patternProperties": { "^.*$": tickerSchema } };
7
+ const s = { "type": "object", "properties": { "herd_conviction_score": { "type": ["number", "null"] }, "user_count": { "type": "number" }, "total_abs_size_today": { "type": "number" } }, "required": ["herd_conviction_score", "user_count", "total_abs_size_today"] };
8
+ return { "type": "object", "patternProperties": { "^.*$": s } };
21
9
  }
22
10
 
23
- static getMetadata() {
24
- return {
25
- type: 'standard',
26
- rootDataDependencies: ['portfolio'],
27
- isHistorical: true,
28
- userType: 'all',
29
- category: 'helix'
30
- };
31
- }
32
-
33
- static getDependencies() { return []; }
34
-
35
- _initAsset(instrumentId) {
36
- if (!this.assetConviction.has(instrumentId)) {
37
- this.assetConviction.set(instrumentId, {
38
- total_pos_size_yesterday: 0, total_pos_size_today: 0,
39
- user_count_yesterday: 0, user_count_today: 0
40
- });
41
- }
42
- }
11
+ _init(id) { if (!this.assetConviction.has(id)) this.assetConviction.set(id, { ySize: 0, tSize: 0, yCount: 0, tCount: 0, hasY: false }); }
43
12
 
44
13
  process(context) {
45
- const { user, mappings, math } = context;
46
- const { extract } = math;
14
+ const { user, mappings, math } = context; const { extract } = math;
47
15
  if (!this.tickerMap) this.tickerMap = mappings.instrumentToTicker;
48
-
49
- const yPos = extract.getPositions(user.portfolio.yesterday, user.type);
50
16
  const tPos = extract.getPositions(user.portfolio.today, user.type);
51
-
52
- const yPosMap = new Map(yPos.map(p => [extract.getInstrumentId(p), p]));
53
- const tPosMap = new Map(tPos.map(p => [extract.getInstrumentId(p), p]));
54
-
55
- const allIds = new Set([...yPosMap.keys(), ...tPosMap.keys()]);
56
-
57
- for (const instId of allIds) {
58
- const tP = tPosMap.get(instId);
59
- const yP = yPosMap.get(instId);
60
-
61
- if (extract.getNetProfit(tP) >= 0) continue; // Only Losers (Herd)
62
-
63
- this._initAsset(instId);
64
- const asset = this.assetConviction.get(instId);
65
- const yWeight = extract.getPositionWeight(yP, user.type);
66
- const tWeight = extract.getPositionWeight(tP, user.type);
67
-
68
- if (yWeight > 0) {
69
- asset.total_pos_size_yesterday += yWeight;
70
- asset.user_count_yesterday++;
71
- }
72
- if (tWeight > 0) {
73
- asset.total_pos_size_today += tWeight;
74
- asset.user_count_today++;
17
+ const yPos = user.portfolio.yesterday ? extract.getPositions(user.portfolio.yesterday, user.type) : [];
18
+ const yMap = new Map(yPos.map(p => [extract.getInstrumentId(p), p]));
19
+ const tMap = new Map(tPos.map(p => [extract.getInstrumentId(p), p]));
20
+ const ids = new Set([...yMap.keys(), ...tMap.keys()]);
21
+ for (const id of ids) {
22
+ const tP = tMap.get(id), yP = yMap.get(id);
23
+ if (extract.getNetProfit(tP) >= 0) continue;
24
+ this._init(id); const a = this.assetConviction.get(id);
25
+ const tW = extract.getPositionWeight(tP, user.type);
26
+ if (tW > 0) { a.tSize += tW; a.tCount++; }
27
+ if (user.portfolio.yesterday) {
28
+ a.hasY = true; const yW = extract.getPositionWeight(yP, user.type);
29
+ if (yW > 0) { a.ySize += yW; a.yCount++; }
75
30
  }
76
31
  }
77
32
  }
78
33
 
79
34
  async getResult() {
80
- if (!this.tickerMap) return {};
81
- const result = {};
82
- for (const [instId, data] of this.assetConviction.entries()) {
83
- const ticker = this.tickerMap[instId];
84
- if (!ticker) continue;
85
-
86
- let avg_pos_change = 0;
87
- if (data.user_count_yesterday > 0 && data.user_count_today > 0) {
88
- const avg_y = data.total_pos_size_yesterday / data.user_count_yesterday;
89
- const avg_t = data.total_pos_size_today / data.user_count_today;
90
- if (avg_y > 0) avg_pos_change = ((avg_t - avg_y) / avg_y) * 100;
91
- } else if (data.user_count_today > 0) avg_pos_change = Infinity;
92
-
93
- if (isFinite(avg_pos_change) && data.user_count_today > 0) {
94
- result[ticker] = {
95
- herd_conviction_score: avg_pos_change,
96
- user_count: data.user_count_today
97
- };
98
- }
35
+ if (!this.tickerMap) return {}; const res = {};
36
+ for (const [id, d] of this.assetConviction.entries()) {
37
+ const tick = this.tickerMap[id]; if (!tick) continue;
38
+ let score = null; if (d.hasY && d.yCount > 0 && d.tCount > 0) score = (((d.tSize / d.tCount) - (d.ySize / d.yCount)) / (d.ySize / d.yCount)) * 100;
39
+ res[tick] = { herd_conviction_score: isFinite(score) ? score : null, user_count: d.tCount, total_abs_size_today: d.tSize };
99
40
  }
100
- return result;
101
- }
102
-
103
- reset() {
104
- this.assetConviction.clear();
105
- this.tickerMap = null;
41
+ return res;
106
42
  }
43
+ reset() { this.assetConviction.clear(); this.tickerMap = null; }
107
44
  }
108
45
  module.exports = HerdConsensusScore;
@@ -1,108 +1,39 @@
1
- /**
2
- * @fileoverview HELIX Product Line (Pass 2)
3
- * REFACTORED: Uses context.math.extract.
4
- */
5
1
  class WinnerLoserFlow {
6
- constructor() {
7
- this.assetFlows = new Map();
8
- this.tickerMap = null;
9
- }
10
-
11
- static getMetadata() {
12
- return {
13
- type: 'standard',
14
- rootDataDependencies: ['portfolio'],
15
- isHistorical: true,
16
- userType: 'all',
17
- category: 'helix'
18
- };
19
- }s
20
-
2
+ constructor() { this.assetFlows = new Map(); this.tickerMap = null; }
3
+ static getMetadata() { return { type: 'standard', rootDataDependencies: ['portfolio'], isHistorical: true, userType: 'all', category: 'helix' }; }
21
4
  static getDependencies() { return []; }
22
5
 
23
6
  static getSchema() {
24
- const tickerSchema = {
25
- "type": "object",
26
- "properties": {
27
- "net_winner_flow": { "type": "number" },
28
- "net_loser_flow": { "type": "number" },
29
- "raw_counts": { "type": "object" }
30
- },
31
- "required": ["net_winner_flow", "net_loser_flow", "raw_counts"]
32
- };
33
- return { "type": "object", "patternProperties": { "^.*$": tickerSchema } };
34
- }
35
-
36
- _getHoldings(positions, extract) {
37
- const map = new Map();
38
- for (const pos of positions) {
39
- const id = extract.getInstrumentId(pos);
40
- if (id) map.set(id, { pnl: extract.getNetProfit(pos) });
41
- }
42
- return map;
43
- }
44
-
45
- _initAsset(ticker) {
46
- if (!this.assetFlows.has(ticker)) {
47
- this.assetFlows.set(ticker, { winners_joined: 0, winners_left: 0, losers_joined: 0, losers_left: 0 });
48
- }
7
+ const s = { "type": "object", "properties": { "net_winner_flow": { "type": ["number", "null"] }, "net_loser_flow": { "type": ["number", "null"] }, "total_winners_today": { "type": "number" }, "total_losers_today": { "type": "number" } }, "required": ["net_winner_flow", "net_loser_flow", "total_winners_today", "total_losers_today"] };
8
+ return { "type": "object", "patternProperties": { "^.*$": s } };
49
9
  }
50
10
 
51
11
  process(context) {
52
- const { user, mappings, math } = context;
53
- const { extract } = math;
12
+ const { user, mappings, math } = context; const { extract } = math;
54
13
  if (!this.tickerMap) this.tickerMap = mappings.instrumentToTicker;
55
-
56
- const yPos = extract.getPositions(user.portfolio.yesterday, user.type);
57
14
  const tPos = extract.getPositions(user.portfolio.today, user.type);
58
-
59
- const yHoldings = this._getHoldings(yPos, extract);
60
- const tHoldings = this._getHoldings(tPos, extract);
61
-
62
- const allIds = new Set([...yHoldings.keys(), ...tHoldings.keys()]);
63
-
64
- for (const instId of allIds) {
65
- const ticker = this.tickerMap[instId];
66
- if (!ticker) continue;
67
-
68
- const tPnl = tHoldings.get(instId)?.pnl || 0;
69
- const isWinner = tPnl > 0;
70
- const isLoser = tPnl < 0;
71
- if (!isWinner && !isLoser) continue;
72
-
73
- const heldYesterday = yHoldings.has(instId);
74
- const holdsToday = tHoldings.has(instId);
75
- if (heldYesterday === holdsToday) continue;
76
-
77
- this._initAsset(ticker);
78
- const stats = this.assetFlows.get(ticker);
79
-
80
- if (isWinner) {
81
- if (holdsToday && !heldYesterday) stats.winners_joined++;
82
- if (!holdsToday && heldYesterday) stats.winners_left++;
83
- }
84
- if (isLoser) {
85
- if (holdsToday && !heldYesterday) stats.losers_joined++;
86
- if (!holdsToday && heldYesterday) stats.losers_left++;
15
+ const yPos = user.portfolio.yesterday ? extract.getPositions(user.portfolio.yesterday, user.type) : [];
16
+ const yH = new Map(yPos.map(p => [extract.getInstrumentId(p), extract.getNetProfit(p)]));
17
+ const tH = new Map(tPos.map(p => [extract.getInstrumentId(p), extract.getNetProfit(p)]));
18
+ const ids = new Set([...yH.keys(), ...tH.keys()]);
19
+ for (const id of ids) {
20
+ const tick = this.tickerMap[id]; if (!tick) continue;
21
+ if (!this.assetFlows.has(tick)) this.assetFlows.set(tick, { wJ: 0, wL: 0, lJ: 0, lL: 0, wT: 0, lT: 0, hasY: !!user.portfolio.yesterday });
22
+ const s = this.assetFlows.get(tick), tPnl = tH.get(id), yPnl = yH.get(id);
23
+ if (tH.has(id)) { if (tPnl > 0) s.wT++; if (tPnl < 0) s.lT++; }
24
+ if (user.portfolio.yesterday) {
25
+ if (tH.has(id) && !yH.has(id)) { if (tPnl > 0) s.wJ++; if (tPnl < 0) s.lJ++; }
26
+ if (!tH.has(id) && yH.has(id)) { if (yPnl > 0) s.wL++; if (yPnl < 0) s.lL++; }
87
27
  }
88
28
  }
89
29
  }
90
30
 
91
31
  async getResult() {
92
- const result = {};
93
- for (const [ticker, data] of this.assetFlows.entries()) {
94
- result[ticker] = {
95
- net_winner_flow: data.winners_joined - data.winners_left,
96
- net_loser_flow: data.losers_joined - data.losers_left,
97
- raw_counts: data
98
- };
32
+ const res = {}; for (const [tick, d] of this.assetFlows.entries()) {
33
+ res[tick] = { net_winner_flow: d.hasY ? d.wJ - d.wL : null, net_loser_flow: d.hasY ? d.lJ - d.lL : null, total_winners_today: d.wT, total_losers_today: d.lT };
99
34
  }
100
- return result;
101
- }
102
-
103
- reset() {
104
- this.assetFlows.clear();
105
- this.tickerMap = null;
35
+ return res;
106
36
  }
37
+ reset() { this.assetFlows.clear(); this.tickerMap = null; }
107
38
  }
108
39
  module.exports = WinnerLoserFlow;