hedgequantx 2.9.20 → 2.9.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/package.json +1 -1
  2. package/src/app.js +64 -42
  3. package/src/menus/connect.js +17 -14
  4. package/src/menus/dashboard.js +76 -58
  5. package/src/pages/accounts.js +49 -38
  6. package/src/pages/ai-agents-ui.js +388 -0
  7. package/src/pages/ai-agents.js +494 -0
  8. package/src/pages/ai-models.js +389 -0
  9. package/src/pages/algo/algo-executor.js +307 -0
  10. package/src/pages/algo/copy-executor.js +331 -0
  11. package/src/pages/algo/copy-trading.js +178 -546
  12. package/src/pages/algo/custom-strategy.js +313 -0
  13. package/src/pages/algo/index.js +75 -18
  14. package/src/pages/algo/one-account.js +57 -322
  15. package/src/pages/algo/ui.js +15 -15
  16. package/src/pages/orders.js +22 -19
  17. package/src/pages/positions.js +22 -19
  18. package/src/pages/stats/index.js +16 -15
  19. package/src/pages/user.js +11 -7
  20. package/src/services/ai-supervision/consensus.js +284 -0
  21. package/src/services/ai-supervision/context.js +275 -0
  22. package/src/services/ai-supervision/directive.js +167 -0
  23. package/src/services/ai-supervision/health.js +47 -35
  24. package/src/services/ai-supervision/index.js +359 -0
  25. package/src/services/ai-supervision/parser.js +278 -0
  26. package/src/services/ai-supervision/symbols.js +259 -0
  27. package/src/services/cliproxy/index.js +256 -0
  28. package/src/services/cliproxy/installer.js +111 -0
  29. package/src/services/cliproxy/manager.js +387 -0
  30. package/src/services/index.js +9 -1
  31. package/src/services/llmproxy/index.js +166 -0
  32. package/src/services/llmproxy/manager.js +411 -0
  33. package/src/services/rithmic/accounts.js +6 -8
  34. package/src/ui/box.js +5 -9
  35. package/src/ui/index.js +18 -5
  36. package/src/ui/menu.js +4 -4
@@ -7,37 +7,40 @@ const ora = require('ora');
7
7
 
8
8
  const { connections } = require('../services');
9
9
  const { ORDER_SIDE } = require('../config');
10
- const { getLogoWidth, drawBoxHeader, drawBoxFooter, drawBoxRow, drawBoxSeparator } = require('../ui');
10
+ const { getLogoWidth, drawBoxHeader, drawBoxFooter, drawBoxRow, drawBoxSeparator, displayBanner, clearScreen } = require('../ui');
11
11
  const { prompts } = require('../utils');
12
12
 
13
13
  /**
14
14
  * Show all open positions
15
15
  */
16
16
  const showPositions = async (service) => {
17
+ // Clear screen and show banner
18
+ clearScreen();
19
+ displayBanner();
20
+
17
21
  const boxWidth = getLogoWidth();
18
22
  let spinner;
19
23
 
20
24
  try {
21
- // Step 1: Get connections
22
- spinner = ora({ text: 'Loading connections...', color: 'yellow' }).start();
25
+ spinner = ora({ text: 'LOADING POSITIONS...', color: 'yellow' }).start();
23
26
 
24
27
  const allConns = connections.count() > 0
25
28
  ? connections.getAll()
26
29
  : (service ? [{ service, propfirm: service.propfirm?.name || 'Unknown', type: 'single' }] : []);
27
30
 
28
31
  if (allConns.length === 0) {
29
- spinner.fail('No connections found');
32
+ spinner.fail('NO CONNECTIONS FOUND');
30
33
  await prompts.waitForEnter();
31
34
  return;
32
35
  }
33
- spinner.succeed(`Found ${allConns.length} connection(s)`);
36
+ spinner.succeed(`FOUND ${allConns.length} CONNECTION(S)`);
34
37
 
35
38
  // Step 2: Fetch accounts
36
39
  let allAccounts = [];
37
40
 
38
41
  for (const conn of allConns) {
39
42
  const propfirmName = conn.propfirm || conn.type || 'Unknown';
40
- spinner = ora({ text: `Fetching accounts from ${propfirmName}...`, color: 'yellow' }).start();
43
+ spinner = ora({ text: `FETCHING ACCOUNTS FROM ${propfirmName.toUpperCase()}...`, color: 'yellow' }).start();
41
44
 
42
45
  try {
43
46
  const result = await conn.service.getTradingAccounts();
@@ -49,17 +52,17 @@ const showPositions = async (service) => {
49
52
  service: conn.service
50
53
  });
51
54
  });
52
- spinner.succeed(`${propfirmName}: ${result.accounts.length} account(s)`);
55
+ spinner.succeed(`${propfirmName.toUpperCase()}: ${result.accounts.length} ACCOUNT(S)`);
53
56
  } else {
54
- spinner.warn(`${propfirmName}: No accounts`);
57
+ spinner.warn(`${propfirmName.toUpperCase()}: NO ACCOUNTS`);
55
58
  }
56
59
  } catch (e) {
57
- spinner.fail(`${propfirmName}: Failed`);
60
+ spinner.fail(`${propfirmName.toUpperCase()}: FAILED`);
58
61
  }
59
62
  }
60
63
 
61
64
  if (allAccounts.length === 0) {
62
- console.log(chalk.yellow('\n No accounts found.'));
65
+ console.log(chalk.yellow('\n NO ACCOUNTS FOUND.'));
63
66
  await prompts.waitForEnter();
64
67
  return;
65
68
  }
@@ -69,7 +72,7 @@ const showPositions = async (service) => {
69
72
 
70
73
  for (const account of allAccounts) {
71
74
  const accName = String(account.accountName || account.rithmicAccountId || account.accountId || 'Unknown').substring(0, 20);
72
- spinner = ora({ text: `Fetching positions for ${accName}...`, color: 'yellow' }).start();
75
+ spinner = ora({ text: `FETCHING POSITIONS FOR ${accName.toUpperCase()}...`, color: 'yellow' }).start();
73
76
 
74
77
  try {
75
78
  const result = await account.service.getPositions(account.accountId);
@@ -81,26 +84,26 @@ const showPositions = async (service) => {
81
84
  propfirm: account.propfirm
82
85
  });
83
86
  });
84
- spinner.succeed(`${accName}: ${result.positions.length} position(s)`);
87
+ spinner.succeed(`${accName.toUpperCase()}: ${result.positions.length} POSITION(S)`);
85
88
  } else {
86
- spinner.succeed(`${accName}: No positions`);
89
+ spinner.succeed(`${accName.toUpperCase()}: NO POSITIONS`);
87
90
  }
88
91
  } catch (e) {
89
- spinner.fail(`${accName}: Failed to fetch positions`);
92
+ spinner.fail(`${accName.toUpperCase()}: FAILED TO FETCH POSITIONS`);
90
93
  }
91
94
  }
92
95
 
93
- spinner = ora({ text: 'Preparing display...', color: 'yellow' }).start();
94
- spinner.succeed(`Total: ${allPositions.length} position(s)`);
96
+ spinner = ora({ text: 'PREPARING DISPLAY...', color: 'yellow' }).start();
97
+ spinner.succeed(`TOTAL: ${allPositions.length} POSITION(S)`);
95
98
  console.log();
96
99
 
97
100
  // Display
98
101
  drawBoxHeader('OPEN POSITIONS', boxWidth);
99
102
 
100
103
  if (allPositions.length === 0) {
101
- drawBoxRow(chalk.gray(' No open positions'), boxWidth);
104
+ drawBoxRow(chalk.gray(' NO OPEN POSITIONS'), boxWidth);
102
105
  } else {
103
- const header = ' ' + 'Symbol'.padEnd(15) + 'Side'.padEnd(8) + 'Size'.padEnd(8) + 'Entry'.padEnd(12) + 'P&L'.padEnd(12) + 'Account';
106
+ const header = ' ' + 'SYMBOL'.padEnd(15) + 'SIDE'.padEnd(8) + 'SIZE'.padEnd(8) + 'ENTRY'.padEnd(12) + 'P&L'.padEnd(12) + 'ACCOUNT';
104
107
  drawBoxRow(chalk.white.bold(header), boxWidth);
105
108
  drawBoxSeparator(boxWidth);
106
109
 
@@ -131,7 +134,7 @@ const showPositions = async (service) => {
131
134
  console.log();
132
135
 
133
136
  } catch (error) {
134
- if (spinner) spinner.fail('Error: ' + error.message);
137
+ if (spinner) spinner.fail('ERROR: ' + error.message.toUpperCase());
135
138
  }
136
139
 
137
140
  await prompts.waitForEnter();
@@ -11,7 +11,7 @@ const ora = require('ora');
11
11
 
12
12
  const { connections } = require('../../services');
13
13
  const { prompts } = require('../../utils');
14
- const { displayBanner } = require('../../ui');
14
+ const { displayBanner , clearScreen } = require('../../ui');
15
15
  const { aggregateStats, calculateDerivedMetrics, calculateQuantMetrics, calculateHQXScore } = require('./metrics');
16
16
  const { renderOverview, renderPnLMetrics, renderQuantMetrics, renderTradesHistory, renderHQXScore, renderNotice } = require('./display');
17
17
  const { renderEquityCurve } = require('./chart');
@@ -124,7 +124,7 @@ const aggregateAccountData = async (activeAccounts) => {
124
124
  } catch (e) {}
125
125
  }
126
126
 
127
- // Trade history (Rithmic doesn't provide this)
127
+ // Trade history from ORDER_PLANT
128
128
  if (typeof svc.getTradeHistory === 'function') {
129
129
  try {
130
130
  const tradesResult = await svc.getTradeHistory(account.accountId, 30);
@@ -137,7 +137,9 @@ const aggregateAccountData = async (activeAccounts) => {
137
137
  connectionType: connType
138
138
  })));
139
139
  }
140
- } catch (e) {}
140
+ } catch (e) {
141
+ // Silent - trade history may not be available
142
+ }
141
143
  }
142
144
  } catch (e) {}
143
145
  }
@@ -159,10 +161,14 @@ const aggregateAccountData = async (activeAccounts) => {
159
161
  * Show Stats Page
160
162
  */
161
163
  const showStats = async (service) => {
164
+ // Clear screen and show banner
165
+ clearScreen();
166
+ displayBanner();
167
+
162
168
  let spinner;
163
169
 
164
170
  try {
165
- spinner = ora({ text: 'Loading stats...', color: 'yellow' }).start();
171
+ spinner = ora({ text: 'LOADING STATS...', color: 'yellow' }).start();
166
172
 
167
173
  // Get all connections
168
174
  const allConns = connections.count() > 0
@@ -184,22 +190,17 @@ const showStats = async (service) => {
184
190
  return;
185
191
  }
186
192
 
187
- // Filter active accounts (status === 0)
188
- const activeAccounts = allAccountsData.filter(acc => acc.status === 0);
189
-
190
- if (activeAccounts.length === 0) {
191
- spinner.fail('No active accounts found');
192
- await prompts.waitForEnter();
193
- return;
194
- }
193
+ // Use all accounts (don't filter by status - Rithmic may have different status formats)
194
+ const activeAccounts = allAccountsData;
195
195
 
196
196
  // Aggregate account data from APIs
197
197
  const accountData = await aggregateAccountData(activeAccounts);
198
198
 
199
- spinner.succeed('Stats loaded');
200
- console.clear();
199
+ spinner.stop();
200
+
201
+ // Clear and show banner before displaying stats
202
+ clearScreen();
201
203
  displayBanner();
202
- console.log();
203
204
 
204
205
  // Calculate stats from API data
205
206
  const stats = aggregateStats(activeAccounts, accountData.allTrades);
package/src/pages/user.js CHANGED
@@ -6,13 +6,17 @@ const chalk = require('chalk');
6
6
  const ora = require('ora');
7
7
 
8
8
  const { connections } = require('../services');
9
- const { getLogoWidth, getColWidths, drawBoxHeader, drawBoxFooter, draw2ColHeader, visibleLength, padText } = require('../ui');
9
+ const { getLogoWidth, getColWidths, drawBoxHeader, drawBoxFooter, draw2ColHeader, visibleLength, padText, displayBanner, clearScreen } = require('../ui');
10
10
  const { prompts } = require('../utils');
11
11
 
12
12
  /**
13
13
  * Show user info
14
14
  */
15
15
  const showUserInfo = async (service) => {
16
+ // Clear screen and show banner
17
+ clearScreen();
18
+ displayBanner();
19
+
16
20
  const boxWidth = getLogoWidth();
17
21
  const { col1, col2 } = getColWidths(boxWidth);
18
22
  let spinner;
@@ -26,7 +30,7 @@ const showUserInfo = async (service) => {
26
30
 
27
31
  try {
28
32
  // Step 1: Get user info
29
- spinner = ora({ text: 'Loading user info...', color: 'yellow' }).start();
33
+ spinner = ora({ text: 'LOADING USER INFO...', color: 'yellow' }).start();
30
34
 
31
35
  let userInfo = null;
32
36
 
@@ -39,10 +43,10 @@ const showUserInfo = async (service) => {
39
43
  } catch (e) {}
40
44
  }
41
45
 
42
- spinner.succeed('User info loaded');
46
+ spinner.succeed('USER INFO LOADED');
43
47
 
44
48
  // Step 2: Get account count
45
- spinner = ora({ text: 'Counting accounts...', color: 'yellow' }).start();
49
+ spinner = ora({ text: 'COUNTING ACCOUNTS...', color: 'yellow' }).start();
46
50
 
47
51
  let accountCount = 0;
48
52
 
@@ -58,14 +62,14 @@ const showUserInfo = async (service) => {
58
62
  } catch (e) {}
59
63
  }
60
64
 
61
- spinner.succeed(`Found ${accountCount} account(s)`);
65
+ spinner.succeed(`FOUND ${accountCount} ACCOUNT(S)`);
62
66
  console.log();
63
67
 
64
68
  // Display
65
69
  drawBoxHeader('USER INFO', boxWidth);
66
70
 
67
71
  if (!userInfo) {
68
- console.log(chalk.cyan('║') + padText(chalk.gray(' No user info available'), boxWidth - 2) + chalk.cyan('║'));
72
+ console.log(chalk.cyan('║') + padText(chalk.gray(' NO USER INFO AVAILABLE'), boxWidth - 2) + chalk.cyan('║'));
69
73
  } else {
70
74
  draw2ColHeader('PROFILE', 'CONNECTIONS', boxWidth);
71
75
 
@@ -90,7 +94,7 @@ const showUserInfo = async (service) => {
90
94
  console.log();
91
95
 
92
96
  } catch (error) {
93
- if (spinner) spinner.fail('Error: ' + error.message);
97
+ if (spinner) spinner.fail('ERROR: ' + error.message.toUpperCase());
94
98
  }
95
99
 
96
100
  await prompts.waitForEnter();
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Consensus Calculator for Multi-Agent Supervision
3
+ *
4
+ * Calculates weighted consensus from multiple AI agent responses.
5
+ * Each agent has a weight, and the final decision is based on
6
+ * the weighted average of all responses.
7
+ */
8
+
9
+ /**
10
+ * Default consensus when no valid responses
11
+ */
12
+ const DEFAULT_CONSENSUS = {
13
+ decision: 'approve',
14
+ confidence: 50,
15
+ optimizations: null,
16
+ reason: 'No consensus - default approve',
17
+ alerts: [],
18
+ agentCount: 0,
19
+ respondedCount: 0,
20
+ unanimous: false
21
+ };
22
+
23
+ /**
24
+ * Calculate weighted average of a numeric field
25
+ */
26
+ const weightedAverage = (values, weights) => {
27
+ if (values.length === 0) return 0;
28
+
29
+ let sum = 0;
30
+ let totalWeight = 0;
31
+
32
+ for (let i = 0; i < values.length; i++) {
33
+ const val = values[i];
34
+ const weight = weights[i] || 1;
35
+ if (val !== null && val !== undefined && !isNaN(val)) {
36
+ sum += val * weight;
37
+ totalWeight += weight;
38
+ }
39
+ }
40
+
41
+ return totalWeight > 0 ? sum / totalWeight : 0;
42
+ };
43
+
44
+ /**
45
+ * Calculate weighted mode (most common value by weight)
46
+ */
47
+ const weightedMode = (values, weights) => {
48
+ if (values.length === 0) return null;
49
+
50
+ const weightMap = {};
51
+
52
+ for (let i = 0; i < values.length; i++) {
53
+ const val = values[i];
54
+ const weight = weights[i] || 1;
55
+ if (val !== null && val !== undefined) {
56
+ weightMap[val] = (weightMap[val] || 0) + weight;
57
+ }
58
+ }
59
+
60
+ let maxWeight = 0;
61
+ let mode = null;
62
+
63
+ for (const [val, w] of Object.entries(weightMap)) {
64
+ if (w > maxWeight) {
65
+ maxWeight = w;
66
+ mode = val;
67
+ }
68
+ }
69
+
70
+ return mode;
71
+ };
72
+
73
+ /**
74
+ * Merge optimizations from multiple agents
75
+ */
76
+ const mergeOptimizations = (responses, weights) => {
77
+ const validOpts = responses
78
+ .map((r, i) => ({ opt: r.optimizations, weight: weights[i] }))
79
+ .filter(o => o.opt !== null);
80
+
81
+ if (validOpts.length === 0) return null;
82
+
83
+ // Collect values for each field
84
+ const entries = validOpts.filter(o => o.opt.entry !== null).map(o => ({ val: o.opt.entry, w: o.weight }));
85
+ const stops = validOpts.filter(o => o.opt.stopLoss !== null).map(o => ({ val: o.opt.stopLoss, w: o.weight }));
86
+ const targets = validOpts.filter(o => o.opt.takeProfit !== null).map(o => ({ val: o.opt.takeProfit, w: o.weight }));
87
+ const sizes = validOpts.filter(o => o.opt.size !== null).map(o => ({ val: o.opt.size, w: o.weight }));
88
+ const timings = validOpts.map(o => ({ val: o.opt.timing, w: o.weight }));
89
+
90
+ return {
91
+ entry: entries.length > 0
92
+ ? Math.round(weightedAverage(entries.map(e => e.val), entries.map(e => e.w)) * 100) / 100
93
+ : null,
94
+ stopLoss: stops.length > 0
95
+ ? Math.round(weightedAverage(stops.map(s => s.val), stops.map(s => s.w)) * 100) / 100
96
+ : null,
97
+ takeProfit: targets.length > 0
98
+ ? Math.round(weightedAverage(targets.map(t => t.val), targets.map(t => t.w)) * 100) / 100
99
+ : null,
100
+ size: sizes.length > 0
101
+ ? Math.round(weightedAverage(sizes.map(s => s.val), sizes.map(s => s.w)) * 100) / 100
102
+ : null,
103
+ timing: weightedMode(timings.map(t => t.val), timings.map(t => t.w)) || 'now'
104
+ };
105
+ };
106
+
107
+ /**
108
+ * Collect all alerts from responses
109
+ */
110
+ const collectAlerts = (responses) => {
111
+ const alerts = [];
112
+ for (const r of responses) {
113
+ if (r.alerts && Array.isArray(r.alerts)) {
114
+ alerts.push(...r.alerts);
115
+ }
116
+ }
117
+ return [...new Set(alerts)].slice(0, 10);
118
+ };
119
+
120
+ /**
121
+ * Build reason summary from all responses
122
+ */
123
+ const buildReasonSummary = (responses, decision) => {
124
+ const reasons = responses
125
+ .filter(r => r.decision === decision && r.reason)
126
+ .map(r => r.reason)
127
+ .slice(0, 3);
128
+
129
+ if (reasons.length === 0) return `Consensus: ${decision}`;
130
+ if (reasons.length === 1) return reasons[0];
131
+
132
+ return reasons[0] + (reasons.length > 1 ? ` (+${reasons.length - 1} more)` : '');
133
+ };
134
+
135
+ /**
136
+ * Calculate consensus from multiple agent responses
137
+ *
138
+ * @param {Array} agentResponses - Array of { agentId, response, weight }
139
+ * @param {Object} options - Consensus options
140
+ * @returns {Object} Consensus result
141
+ */
142
+ const calculateConsensus = (agentResponses, options = {}) => {
143
+ const {
144
+ minAgents = 1,
145
+ approveThreshold = 0.5,
146
+ rejectThreshold = 0.6,
147
+ minConfidence = 30
148
+ } = options;
149
+
150
+ // Filter valid responses
151
+ const validResponses = agentResponses.filter(ar =>
152
+ ar && ar.response && ar.response.decision
153
+ );
154
+
155
+ if (validResponses.length === 0) {
156
+ return { ...DEFAULT_CONSENSUS, reason: 'No valid agent responses' };
157
+ }
158
+
159
+ if (validResponses.length < minAgents) {
160
+ return {
161
+ ...DEFAULT_CONSENSUS,
162
+ reason: `Insufficient agents (${validResponses.length}/${minAgents})`,
163
+ agentCount: agentResponses.length,
164
+ respondedCount: validResponses.length
165
+ };
166
+ }
167
+
168
+ // Extract responses and weights
169
+ const responses = validResponses.map(ar => ar.response);
170
+ const weights = validResponses.map(ar => ar.weight || 100);
171
+ const totalWeight = weights.reduce((a, b) => a + b, 0);
172
+
173
+ // Calculate weighted votes for each decision
174
+ const votes = { approve: 0, reject: 0, modify: 0 };
175
+ for (let i = 0; i < responses.length; i++) {
176
+ const decision = responses[i].decision;
177
+ votes[decision] = (votes[decision] || 0) + weights[i];
178
+ }
179
+
180
+ // Normalize votes to percentages
181
+ const approveRatio = votes.approve / totalWeight;
182
+ const rejectRatio = votes.reject / totalWeight;
183
+ const modifyRatio = votes.modify / totalWeight;
184
+
185
+ // Determine consensus decision
186
+ let decision;
187
+ if (rejectRatio >= rejectThreshold) {
188
+ decision = 'reject';
189
+ } else if (approveRatio >= approveThreshold) {
190
+ decision = modifyRatio > 0 ? 'modify' : 'approve';
191
+ } else if (modifyRatio > approveRatio) {
192
+ decision = 'modify';
193
+ } else {
194
+ decision = 'approve';
195
+ }
196
+
197
+ // Calculate weighted confidence
198
+ const confidences = responses.map(r => r.confidence);
199
+ const avgConfidence = Math.round(weightedAverage(confidences, weights));
200
+
201
+ // Apply minimum confidence check
202
+ if (avgConfidence < minConfidence && decision !== 'reject') {
203
+ decision = 'reject';
204
+ }
205
+
206
+ // Check unanimity
207
+ const decisions = responses.map(r => r.decision);
208
+ const unanimous = new Set(decisions).size === 1;
209
+
210
+ // Build consensus result
211
+ const consensus = {
212
+ decision,
213
+ confidence: avgConfidence,
214
+ optimizations: decision !== 'reject' ? mergeOptimizations(responses, weights) : null,
215
+ reason: buildReasonSummary(responses, decision),
216
+ alerts: collectAlerts(responses),
217
+
218
+ // Metadata
219
+ agentCount: agentResponses.length,
220
+ respondedCount: validResponses.length,
221
+ unanimous,
222
+
223
+ // Vote breakdown
224
+ votes: {
225
+ approve: Math.round(approveRatio * 100),
226
+ reject: Math.round(rejectRatio * 100),
227
+ modify: Math.round(modifyRatio * 100)
228
+ },
229
+
230
+ // Individual responses for debugging
231
+ agentDetails: validResponses.map(ar => ({
232
+ agentId: ar.agentId,
233
+ decision: ar.response.decision,
234
+ confidence: ar.response.confidence,
235
+ weight: ar.weight
236
+ }))
237
+ };
238
+
239
+ return consensus;
240
+ };
241
+
242
+ /**
243
+ * Quick check if consensus approves the signal
244
+ */
245
+ const isApproved = (consensus) => {
246
+ return consensus.decision === 'approve' || consensus.decision === 'modify';
247
+ };
248
+
249
+ /**
250
+ * Apply consensus optimizations to original signal
251
+ */
252
+ const applyOptimizations = (signal, consensus) => {
253
+ if (!isApproved(consensus) || !consensus.optimizations) {
254
+ return signal;
255
+ }
256
+
257
+ const opts = consensus.optimizations;
258
+ const optimized = { ...signal };
259
+
260
+ if (opts.entry !== null) optimized.entry = opts.entry;
261
+ if (opts.stopLoss !== null) optimized.stopLoss = opts.stopLoss;
262
+ if (opts.takeProfit !== null) optimized.takeProfit = opts.takeProfit;
263
+
264
+ // Apply size adjustment
265
+ if (opts.size !== null && signal.size) {
266
+ optimized.size = Math.max(1, Math.round(signal.size * (1 + opts.size)));
267
+ }
268
+
269
+ optimized.aiOptimized = true;
270
+ optimized.aiConfidence = consensus.confidence;
271
+ optimized.aiTiming = opts.timing;
272
+
273
+ return optimized;
274
+ };
275
+
276
+ module.exports = {
277
+ calculateConsensus,
278
+ isApproved,
279
+ applyOptimizations,
280
+ weightedAverage,
281
+ weightedMode,
282
+ mergeOptimizations,
283
+ DEFAULT_CONSENSUS
284
+ };