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.
- package/package.json +1 -1
- package/src/app.js +64 -42
- package/src/menus/connect.js +17 -14
- package/src/menus/dashboard.js +76 -58
- package/src/pages/accounts.js +49 -38
- package/src/pages/ai-agents-ui.js +388 -0
- package/src/pages/ai-agents.js +494 -0
- package/src/pages/ai-models.js +389 -0
- package/src/pages/algo/algo-executor.js +307 -0
- package/src/pages/algo/copy-executor.js +331 -0
- package/src/pages/algo/copy-trading.js +178 -546
- package/src/pages/algo/custom-strategy.js +313 -0
- package/src/pages/algo/index.js +75 -18
- package/src/pages/algo/one-account.js +57 -322
- package/src/pages/algo/ui.js +15 -15
- package/src/pages/orders.js +22 -19
- package/src/pages/positions.js +22 -19
- package/src/pages/stats/index.js +16 -15
- package/src/pages/user.js +11 -7
- package/src/services/ai-supervision/consensus.js +284 -0
- package/src/services/ai-supervision/context.js +275 -0
- package/src/services/ai-supervision/directive.js +167 -0
- package/src/services/ai-supervision/health.js +47 -35
- package/src/services/ai-supervision/index.js +359 -0
- package/src/services/ai-supervision/parser.js +278 -0
- package/src/services/ai-supervision/symbols.js +259 -0
- package/src/services/cliproxy/index.js +256 -0
- package/src/services/cliproxy/installer.js +111 -0
- package/src/services/cliproxy/manager.js +387 -0
- package/src/services/index.js +9 -1
- package/src/services/llmproxy/index.js +166 -0
- package/src/services/llmproxy/manager.js +411 -0
- package/src/services/rithmic/accounts.js +6 -8
- package/src/ui/box.js +5 -9
- package/src/ui/index.js +18 -5
- package/src/ui/menu.js +4 -4
package/src/pages/positions.js
CHANGED
|
@@ -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
|
-
|
|
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('
|
|
32
|
+
spinner.fail('NO CONNECTIONS FOUND');
|
|
30
33
|
await prompts.waitForEnter();
|
|
31
34
|
return;
|
|
32
35
|
}
|
|
33
|
-
spinner.succeed(`
|
|
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: `
|
|
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}
|
|
55
|
+
spinner.succeed(`${propfirmName.toUpperCase()}: ${result.accounts.length} ACCOUNT(S)`);
|
|
53
56
|
} else {
|
|
54
|
-
spinner.warn(`${propfirmName}:
|
|
57
|
+
spinner.warn(`${propfirmName.toUpperCase()}: NO ACCOUNTS`);
|
|
55
58
|
}
|
|
56
59
|
} catch (e) {
|
|
57
|
-
spinner.fail(`${propfirmName}:
|
|
60
|
+
spinner.fail(`${propfirmName.toUpperCase()}: FAILED`);
|
|
58
61
|
}
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
if (allAccounts.length === 0) {
|
|
62
|
-
console.log(chalk.yellow('\n
|
|
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: `
|
|
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}
|
|
87
|
+
spinner.succeed(`${accName.toUpperCase()}: ${result.positions.length} POSITION(S)`);
|
|
85
88
|
} else {
|
|
86
|
-
spinner.succeed(`${accName}:
|
|
89
|
+
spinner.succeed(`${accName.toUpperCase()}: NO POSITIONS`);
|
|
87
90
|
}
|
|
88
91
|
} catch (e) {
|
|
89
|
-
spinner.fail(`${accName}:
|
|
92
|
+
spinner.fail(`${accName.toUpperCase()}: FAILED TO FETCH POSITIONS`);
|
|
90
93
|
}
|
|
91
94
|
}
|
|
92
95
|
|
|
93
|
-
spinner = ora({ text: '
|
|
94
|
-
spinner.succeed(`
|
|
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('
|
|
104
|
+
drawBoxRow(chalk.gray(' NO OPEN POSITIONS'), boxWidth);
|
|
102
105
|
} else {
|
|
103
|
-
const header = ' ' + '
|
|
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('
|
|
137
|
+
if (spinner) spinner.fail('ERROR: ' + error.message.toUpperCase());
|
|
135
138
|
}
|
|
136
139
|
|
|
137
140
|
await prompts.waitForEnter();
|
package/src/pages/stats/index.js
CHANGED
|
@@ -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
|
|
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: '
|
|
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
|
-
//
|
|
188
|
-
const activeAccounts = allAccountsData
|
|
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.
|
|
200
|
-
|
|
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: '
|
|
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('
|
|
46
|
+
spinner.succeed('USER INFO LOADED');
|
|
43
47
|
|
|
44
48
|
// Step 2: Get account count
|
|
45
|
-
spinner = ora({ text: '
|
|
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(`
|
|
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('
|
|
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('
|
|
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
|
+
};
|