@exagent/agent 0.3.0 → 0.3.1
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/.turbo/turbo-build.log +6 -6
- package/dist/chunk-4UVMO6ZM.js +6318 -0
- package/dist/chunk-5NU6FDDE.js +6020 -0
- package/dist/chunk-GPMXUMYH.js +5991 -0
- package/dist/chunk-IJK4EFTJ.js +6043 -0
- package/dist/chunk-J3NG7AGT.js +6047 -0
- package/dist/chunk-QG22GADV.js +6316 -0
- package/dist/chunk-SVFTC5V2.js +6021 -0
- package/dist/chunk-VDK4XPAC.js +6318 -0
- package/dist/cli.js +252 -129
- package/dist/index.d.ts +15 -0
- package/dist/index.js +1 -1
- package/package.json +8 -1
- package/src/cli.ts +33 -15
- package/src/config.ts +6 -0
- package/src/runtime.ts +370 -37
- package/src/scrub-secrets.ts +39 -0
- package/src/setup.ts +228 -129
- package/src/strategy/loader.ts +35 -2
- package/src/ui.ts +75 -0
package/src/cli.ts
CHANGED
|
@@ -4,13 +4,14 @@ import { loadConfig, writeSampleConfig } from './config.js';
|
|
|
4
4
|
import { AgentRuntime } from './runtime.js';
|
|
5
5
|
import { listTemplates } from './strategy/index.js';
|
|
6
6
|
import { ensureLocalSetup, promptSecretPassword } from './setup.js';
|
|
7
|
+
import { printBanner, printDone, printError, printSuccess, pc } from './ui.js';
|
|
7
8
|
|
|
8
9
|
const program = new Command();
|
|
9
10
|
|
|
10
11
|
program
|
|
11
12
|
.name('exagent')
|
|
12
13
|
.description('Exagent — LLM trading agent runtime')
|
|
13
|
-
.version('0.
|
|
14
|
+
.version('0.3.0');
|
|
14
15
|
|
|
15
16
|
program
|
|
16
17
|
.command('init')
|
|
@@ -19,10 +20,13 @@ program
|
|
|
19
20
|
.option('--api-url <url>', 'API server URL', 'http://localhost:3002')
|
|
20
21
|
.option('--config <path>', 'Config file path', 'agent-config.json')
|
|
21
22
|
.action((opts) => {
|
|
23
|
+
printBanner();
|
|
22
24
|
writeSampleConfig(opts.agentId, opts.apiUrl, opts.config);
|
|
23
|
-
|
|
24
|
-
console.log(
|
|
25
|
-
console.log(`
|
|
25
|
+
printDone(`Created ${pc.cyan(opts.config)}`);
|
|
26
|
+
console.log();
|
|
27
|
+
console.log(` Edit only the public strategy/venue/risk settings.`);
|
|
28
|
+
console.log(` Then run: ${pc.cyan(`exagent setup --config ${opts.config}`)}`);
|
|
29
|
+
console.log();
|
|
26
30
|
});
|
|
27
31
|
|
|
28
32
|
program
|
|
@@ -32,9 +36,8 @@ program
|
|
|
32
36
|
.action(async (opts) => {
|
|
33
37
|
try {
|
|
34
38
|
await ensureLocalSetup(opts.config);
|
|
35
|
-
console.log('Secure local setup complete.');
|
|
36
39
|
} catch (err) {
|
|
37
|
-
|
|
40
|
+
printError((err as Error).message);
|
|
38
41
|
process.exit(1);
|
|
39
42
|
}
|
|
40
43
|
});
|
|
@@ -46,16 +49,19 @@ program
|
|
|
46
49
|
.action(async (opts) => {
|
|
47
50
|
try {
|
|
48
51
|
await ensureLocalSetup(opts.config);
|
|
52
|
+
printBanner();
|
|
49
53
|
const config = await loadConfig(opts.config, {
|
|
50
54
|
getSecretPassword: async () => promptSecretPassword(),
|
|
51
55
|
});
|
|
56
|
+
printDone(`Agent ${pc.cyan(config.agentId)} starting...`);
|
|
57
|
+
console.log();
|
|
52
58
|
const runtime = new AgentRuntime(config);
|
|
53
59
|
await runtime.start();
|
|
54
60
|
|
|
55
61
|
// Keep the process running
|
|
56
62
|
await new Promise(() => {});
|
|
57
63
|
} catch (err) {
|
|
58
|
-
|
|
64
|
+
printError((err as Error).message);
|
|
59
65
|
process.exit(1);
|
|
60
66
|
}
|
|
61
67
|
});
|
|
@@ -64,12 +70,14 @@ program
|
|
|
64
70
|
.command('templates')
|
|
65
71
|
.description('List available strategy templates')
|
|
66
72
|
.action(() => {
|
|
73
|
+
printBanner();
|
|
67
74
|
const templates = listTemplates();
|
|
68
|
-
console.log('
|
|
75
|
+
console.log(pc.bold(' Strategy Templates'));
|
|
76
|
+
console.log();
|
|
69
77
|
for (const t of templates) {
|
|
70
|
-
console.log(` ${t.id}`);
|
|
71
|
-
console.log(`
|
|
72
|
-
console.log(`
|
|
78
|
+
console.log(` ${pc.cyan(t.id)}`);
|
|
79
|
+
console.log(` ${t.name} — ${pc.dim(t.description)}`);
|
|
80
|
+
console.log(` ${pc.dim(`Risk: ${t.riskLevel} | Venues: ${t.venues.join(', ') || 'any'}`)}`);
|
|
73
81
|
console.log();
|
|
74
82
|
}
|
|
75
83
|
});
|
|
@@ -81,6 +89,7 @@ program
|
|
|
81
89
|
.action(async (opts) => {
|
|
82
90
|
try {
|
|
83
91
|
await ensureLocalSetup(opts.config);
|
|
92
|
+
printBanner();
|
|
84
93
|
const config = await loadConfig(opts.config, {
|
|
85
94
|
getSecretPassword: async () => promptSecretPassword(),
|
|
86
95
|
});
|
|
@@ -89,14 +98,23 @@ program
|
|
|
89
98
|
});
|
|
90
99
|
|
|
91
100
|
if (!res.ok) {
|
|
92
|
-
|
|
101
|
+
printError(`API error: ${res.status}`);
|
|
93
102
|
process.exit(1);
|
|
94
103
|
}
|
|
95
104
|
|
|
96
|
-
const agent = await res.json()
|
|
97
|
-
|
|
105
|
+
const agent = await res.json() as Record<string, unknown>;
|
|
106
|
+
const name = (agent.name as string) || config.agentId;
|
|
107
|
+
const status = (agent.status as string) || 'unknown';
|
|
108
|
+
|
|
109
|
+
const statusColor = status === 'online' ? pc.green : status === 'error' ? pc.red : pc.yellow;
|
|
110
|
+
|
|
111
|
+
printSuccess(name, [
|
|
112
|
+
`${pc.dim('Status:')} ${statusColor(status)}`,
|
|
113
|
+
`${pc.dim('Agent:')} ${pc.cyan(config.agentId)}`,
|
|
114
|
+
`${pc.dim('API:')} ${pc.dim(config.apiUrl)}`,
|
|
115
|
+
]);
|
|
98
116
|
} catch (err) {
|
|
99
|
-
|
|
117
|
+
printError((err as Error).message);
|
|
100
118
|
process.exit(1);
|
|
101
119
|
}
|
|
102
120
|
});
|
package/src/config.ts
CHANGED
|
@@ -22,7 +22,9 @@ export interface RuntimeConfig {
|
|
|
22
22
|
};
|
|
23
23
|
strategy: {
|
|
24
24
|
file?: string;
|
|
25
|
+
code?: string;
|
|
25
26
|
template?: string;
|
|
27
|
+
venues?: string[];
|
|
26
28
|
prompt?: {
|
|
27
29
|
name?: string;
|
|
28
30
|
systemPrompt: string;
|
|
@@ -132,7 +134,9 @@ const runtimeSchema = z.object({
|
|
|
132
134
|
}),
|
|
133
135
|
strategy: z.object({
|
|
134
136
|
file: z.string().optional(),
|
|
137
|
+
code: z.string().optional(),
|
|
135
138
|
template: z.string().optional(),
|
|
139
|
+
venues: z.array(z.string()).optional(),
|
|
136
140
|
prompt: z.object({
|
|
137
141
|
name: z.string().optional(),
|
|
138
142
|
systemPrompt: z.string().min(1),
|
|
@@ -214,7 +218,9 @@ const configFileSchema = z.object({
|
|
|
214
218
|
}).default({}),
|
|
215
219
|
strategy: z.object({
|
|
216
220
|
file: z.string().optional(),
|
|
221
|
+
code: z.string().optional(),
|
|
217
222
|
template: z.string().optional(),
|
|
223
|
+
venues: z.array(z.string()).optional(),
|
|
218
224
|
prompt: z.object({
|
|
219
225
|
name: z.string().optional(),
|
|
220
226
|
systemPrompt: z.string().min(1),
|
package/src/runtime.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { getLogger, configureLogger } from './logger.js';
|
|
|
26
26
|
import type { LogLevel } from './logger.js';
|
|
27
27
|
import { DiagnosticsCollector } from './diagnostics.js';
|
|
28
28
|
import type { CycleTimings } from './diagnostics.js';
|
|
29
|
+
import { scrubSecrets } from './scrub-secrets.js';
|
|
29
30
|
|
|
30
31
|
// Hyperliquid
|
|
31
32
|
import { HyperliquidClient } from './perp/client.js';
|
|
@@ -49,7 +50,10 @@ import type { SpotConfig } from './spot/types.js';
|
|
|
49
50
|
import { BridgeManager } from './bridge/bridge-manager.js';
|
|
50
51
|
import type { BridgeConfig } from './bridge/types.js';
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
import { createRequire } from 'module';
|
|
54
|
+
const _require = createRequire(import.meta.url);
|
|
55
|
+
let SDK_VERSION = '0.3.0';
|
|
56
|
+
try { SDK_VERSION = _require('../package.json').version; } catch {}
|
|
53
57
|
|
|
54
58
|
/** Number of consecutive cycle failures before switching to idle */
|
|
55
59
|
const MAX_CONSECUTIVE_FAILURES = 3;
|
|
@@ -121,18 +125,7 @@ export class AgentRuntime {
|
|
|
121
125
|
|
|
122
126
|
this.diagnostics = new DiagnosticsCollector();
|
|
123
127
|
this.llm = createLLMAdapter(config.llm);
|
|
124
|
-
|
|
125
|
-
// Apply LLM daily token budget if configured
|
|
126
|
-
if (config.llmBudget?.maxDailyTokens && this.llm instanceof BaseLLMAdapter) {
|
|
127
|
-
this.llm.setMaxDailyTokens(config.llmBudget.maxDailyTokens);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Wire LLM call recording into diagnostics
|
|
131
|
-
if (this.llm instanceof BaseLLMAdapter) {
|
|
132
|
-
this.llm.setCallRecordCallback((record) => {
|
|
133
|
-
this.diagnostics.recordLLMCall(record);
|
|
134
|
-
});
|
|
135
|
-
}
|
|
128
|
+
this.configureLLMAdapter(this.llm);
|
|
136
129
|
|
|
137
130
|
this.relay = new RelayClient({
|
|
138
131
|
url: config.relay.url,
|
|
@@ -236,6 +229,258 @@ export class AgentRuntime {
|
|
|
236
229
|
process.exit(0);
|
|
237
230
|
}
|
|
238
231
|
|
|
232
|
+
private configureLLMAdapter(adapter: LLMAdapter): void {
|
|
233
|
+
if (this.config.llmBudget?.maxDailyTokens && adapter instanceof BaseLLMAdapter) {
|
|
234
|
+
adapter.setMaxDailyTokens(this.config.llmBudget.maxDailyTokens);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (adapter instanceof BaseLLMAdapter) {
|
|
238
|
+
adapter.setCallRecordCallback((record) => {
|
|
239
|
+
this.diagnostics.recordLLMCall(record);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private teardownVenues(): void {
|
|
245
|
+
if (this.hlWs) {
|
|
246
|
+
this.hlWs.disconnect();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
this.hlWs = null;
|
|
250
|
+
this.hlClient = null;
|
|
251
|
+
this.hlSigner = null;
|
|
252
|
+
this.hlOrders = null;
|
|
253
|
+
this.hlPositions = null;
|
|
254
|
+
this.pmClient = null;
|
|
255
|
+
this.pmOrders = null;
|
|
256
|
+
this.spotManager = null;
|
|
257
|
+
this.bridgeManager = null;
|
|
258
|
+
this.walletAddress = null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private async waitForCycleCompletion(timeoutMs: number = 30000): Promise<void> {
|
|
262
|
+
let waitedMs = 0;
|
|
263
|
+
while (this.cycleInProgress && waitedMs < timeoutMs) {
|
|
264
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
265
|
+
waitedMs += 100;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private getConfiguredSpotVenue(): 'aerodrome' | 'uniswap' {
|
|
270
|
+
return this.config.venues?.spot?.defaultChain === 'base' ? 'aerodrome' : 'uniswap';
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private normalizeVenueForExecution(venue?: string): string | undefined {
|
|
274
|
+
if (!venue) return undefined;
|
|
275
|
+
|
|
276
|
+
if (venue === 'hyperliquid_spot') {
|
|
277
|
+
return this.getConfiguredSpotVenue();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (venue === 'sushiswap') {
|
|
281
|
+
return 'uniswap';
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return venue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private isVenueConfigured(venue: string): boolean {
|
|
288
|
+
const normalized = this.normalizeVenueForExecution(venue) ?? venue;
|
|
289
|
+
|
|
290
|
+
switch (normalized) {
|
|
291
|
+
case 'hyperliquid_perp':
|
|
292
|
+
case 'hyperliquid_deposit':
|
|
293
|
+
case 'hyperliquid_withdraw':
|
|
294
|
+
return this.config.venues?.hyperliquid_perp?.enabled === true;
|
|
295
|
+
case 'polymarket':
|
|
296
|
+
return this.config.venues?.polymarket?.enabled === true;
|
|
297
|
+
case 'uniswap':
|
|
298
|
+
case 'aerodrome':
|
|
299
|
+
return this.config.venues?.spot?.enabled === true;
|
|
300
|
+
case 'across':
|
|
301
|
+
return this.config.venues?.bridge?.enabled === true;
|
|
302
|
+
default:
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private canExecuteVenue(venue: string): boolean {
|
|
308
|
+
const normalized = this.normalizeVenueForExecution(venue) ?? venue;
|
|
309
|
+
if (!this.isVenueConfigured(normalized)) {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (this.paper) {
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
switch (normalized) {
|
|
318
|
+
case 'hyperliquid_perp':
|
|
319
|
+
case 'hyperliquid_deposit':
|
|
320
|
+
case 'hyperliquid_withdraw':
|
|
321
|
+
return !!this.hlOrders || !!this.hlSigner;
|
|
322
|
+
case 'polymarket':
|
|
323
|
+
return !!this.pmOrders;
|
|
324
|
+
case 'uniswap':
|
|
325
|
+
case 'aerodrome':
|
|
326
|
+
return !!this.spotManager;
|
|
327
|
+
case 'across':
|
|
328
|
+
return !!this.bridgeManager;
|
|
329
|
+
default:
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private getPreferredExecutionVenues(): string[] {
|
|
335
|
+
const preferred = [
|
|
336
|
+
...(this.config.strategy.venues ?? []),
|
|
337
|
+
...(this.config.strategy.prompt?.venues ?? []),
|
|
338
|
+
];
|
|
339
|
+
|
|
340
|
+
const normalized = preferred
|
|
341
|
+
.map((venue) => this.normalizeVenueForExecution(venue))
|
|
342
|
+
.filter((venue): venue is string => Boolean(venue));
|
|
343
|
+
|
|
344
|
+
const fallback: string[] = [];
|
|
345
|
+
if (this.config.venues?.hyperliquid_perp?.enabled) fallback.push('hyperliquid_perp');
|
|
346
|
+
if (this.config.venues?.polymarket?.enabled) fallback.push('polymarket');
|
|
347
|
+
if (this.config.venues?.spot?.enabled) fallback.push(this.getConfiguredSpotVenue());
|
|
348
|
+
if (this.config.venues?.bridge?.enabled) fallback.push('across');
|
|
349
|
+
|
|
350
|
+
return [...new Set([...normalized, ...fallback])];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private buildRuntimeVenuesFromSelection(selectedVenues: string[]): NonNullable<RuntimeConfig['venues']> {
|
|
354
|
+
const selected = new Set(selectedVenues);
|
|
355
|
+
const current = this.config.venues ?? {};
|
|
356
|
+
const spotEnabled = selectedVenues.some((venue) => ['hyperliquid_spot', 'uniswap', 'aerodrome', 'sushiswap'].includes(venue));
|
|
357
|
+
const multiChainSpot = selected.has('uniswap') || selected.has('sushiswap');
|
|
358
|
+
const defaultSpotChains = multiChainSpot
|
|
359
|
+
? ['base', 'ethereum', 'arbitrum', 'polygon']
|
|
360
|
+
: ['base'];
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
hyperliquid_perp: {
|
|
364
|
+
enabled: selected.has('hyperliquid_perp'),
|
|
365
|
+
apiUrl: current.hyperliquid_perp?.apiUrl ?? 'https://api.hyperliquid.xyz',
|
|
366
|
+
wsUrl: current.hyperliquid_perp?.wsUrl ?? 'wss://api.hyperliquid.xyz/ws',
|
|
367
|
+
maxLeverage: current.hyperliquid_perp?.maxLeverage ?? 10,
|
|
368
|
+
maxNotionalUSD: current.hyperliquid_perp?.maxNotionalUSD ?? 50000,
|
|
369
|
+
allowedInstruments: current.hyperliquid_perp?.allowedInstruments,
|
|
370
|
+
},
|
|
371
|
+
polymarket: {
|
|
372
|
+
enabled: selected.has('polymarket'),
|
|
373
|
+
clobApiUrl: current.polymarket?.clobApiUrl ?? 'https://clob.polymarket.com',
|
|
374
|
+
gammaApiUrl: current.polymarket?.gammaApiUrl ?? 'https://gamma-api.polymarket.com',
|
|
375
|
+
maxNotionalUSD: current.polymarket?.maxNotionalUSD ?? 1000,
|
|
376
|
+
maxTotalExposureUSD: current.polymarket?.maxTotalExposureUSD ?? 5000,
|
|
377
|
+
allowedCategories: current.polymarket?.allowedCategories,
|
|
378
|
+
},
|
|
379
|
+
spot: {
|
|
380
|
+
enabled: spotEnabled,
|
|
381
|
+
chains: current.spot?.chains?.length ? Array.from(new Set([...defaultSpotChains, ...current.spot.chains])) : defaultSpotChains,
|
|
382
|
+
defaultChain: current.spot?.defaultChain ?? 'base',
|
|
383
|
+
maxSlippageBps: current.spot?.maxSlippageBps ?? this.config.trading.maxSlippageBps,
|
|
384
|
+
maxSwapValueUSD: current.spot?.maxSwapValueUSD ?? 10000,
|
|
385
|
+
},
|
|
386
|
+
bridge: {
|
|
387
|
+
enabled: selected.has('across'),
|
|
388
|
+
defaultBridge: current.bridge?.defaultBridge ?? 'across',
|
|
389
|
+
maxBridgeValueUSD: current.bridge?.maxBridgeValueUSD ?? 10000,
|
|
390
|
+
fillTimeoutMs: current.bridge?.fillTimeoutMs ?? 300000,
|
|
391
|
+
pollIntervalMs: current.bridge?.pollIntervalMs ?? 2000,
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private buildStrategyFallbackPrompt(strategy: {
|
|
397
|
+
name?: string;
|
|
398
|
+
description?: string;
|
|
399
|
+
category?: string;
|
|
400
|
+
venues?: string[];
|
|
401
|
+
}): string {
|
|
402
|
+
const name = strategy.name?.trim() || 'Dashboard Strategy';
|
|
403
|
+
const description = strategy.description?.trim();
|
|
404
|
+
const category = strategy.category?.trim();
|
|
405
|
+
const venues = strategy.venues?.length ? strategy.venues.join(', ') : 'any';
|
|
406
|
+
|
|
407
|
+
return [
|
|
408
|
+
`You are the "${name}" trading agent.`,
|
|
409
|
+
category ? `Strategy category: ${category}.` : null,
|
|
410
|
+
description
|
|
411
|
+
? `Strategy description: ${description}`
|
|
412
|
+
: 'Strategy description: follow the owner-configured strategy exactly and do not improvise outside it.',
|
|
413
|
+
`Allowed venues: ${venues}.`,
|
|
414
|
+
'Stay inside the owner-configured scope. If the setup is unclear or the trade does not fit, return no trades.',
|
|
415
|
+
'Return ONLY a JSON array of trade signals.',
|
|
416
|
+
].filter((line): line is string => Boolean(line)).join('\n');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private extractStrategyConfigFromAgentConfig(cfg: Record<string, unknown>): RuntimeConfig['strategy'] {
|
|
420
|
+
const rawStrategy = cfg.strategy as Record<string, unknown> | undefined;
|
|
421
|
+
if (!rawStrategy) {
|
|
422
|
+
return { template: 'hold' };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const venues = Array.isArray(rawStrategy.venues)
|
|
426
|
+
? rawStrategy.venues.filter((venue): venue is string => typeof venue === 'string')
|
|
427
|
+
: undefined;
|
|
428
|
+
|
|
429
|
+
if (typeof rawStrategy.file === 'string' && rawStrategy.file.trim()) {
|
|
430
|
+
return { file: rawStrategy.file, venues };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (typeof rawStrategy.code === 'string' && rawStrategy.code.trim()) {
|
|
434
|
+
return { code: rawStrategy.code, venues };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (typeof rawStrategy.systemPrompt === 'string' && rawStrategy.systemPrompt.trim()) {
|
|
438
|
+
return {
|
|
439
|
+
venues,
|
|
440
|
+
prompt: {
|
|
441
|
+
name: typeof rawStrategy.name === 'string' ? rawStrategy.name : undefined,
|
|
442
|
+
systemPrompt: rawStrategy.systemPrompt,
|
|
443
|
+
venues,
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (typeof rawStrategy.template === 'string' && rawStrategy.template.trim()) {
|
|
449
|
+
return {
|
|
450
|
+
template: rawStrategy.template,
|
|
451
|
+
venues,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
venues,
|
|
457
|
+
prompt: {
|
|
458
|
+
name: typeof rawStrategy.name === 'string' ? rawStrategy.name : undefined,
|
|
459
|
+
systemPrompt: this.buildStrategyFallbackPrompt({
|
|
460
|
+
name: typeof rawStrategy.name === 'string' ? rawStrategy.name : undefined,
|
|
461
|
+
description: typeof rawStrategy.description === 'string' ? rawStrategy.description : undefined,
|
|
462
|
+
category: typeof rawStrategy.category === 'string' ? rawStrategy.category : undefined,
|
|
463
|
+
venues,
|
|
464
|
+
}),
|
|
465
|
+
venues,
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private async applyExecutionMode(): Promise<void> {
|
|
471
|
+
if (this.config.trading.mode === 'paper') {
|
|
472
|
+
this.teardownVenues();
|
|
473
|
+
if (!this.paper) {
|
|
474
|
+
this.paper = new PaperExecutor(this.config.trading.initialCapitalUSD ?? 10000);
|
|
475
|
+
}
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
this.paper = null;
|
|
480
|
+
this.teardownVenues();
|
|
481
|
+
await this.initializeVenues();
|
|
482
|
+
}
|
|
483
|
+
|
|
239
484
|
// ── VENUE INITIALIZATION ───────────────────────────────────
|
|
240
485
|
|
|
241
486
|
private async initializeVenues(): Promise<void> {
|
|
@@ -476,8 +721,9 @@ export class AgentRuntime {
|
|
|
476
721
|
store: this.store,
|
|
477
722
|
config: this.config.trading,
|
|
478
723
|
log: (msg: string) => {
|
|
479
|
-
|
|
480
|
-
|
|
724
|
+
const safe = scrubSecrets(msg);
|
|
725
|
+
log.info('strategy', safe);
|
|
726
|
+
this.signal.reportInfo('Strategy Log', safe);
|
|
481
727
|
},
|
|
482
728
|
});
|
|
483
729
|
timings.strategyMs = Date.now() - strategyStart;
|
|
@@ -598,11 +844,20 @@ export class AgentRuntime {
|
|
|
598
844
|
// ── SIGNAL EXECUTION ───────────────────────────────────────
|
|
599
845
|
|
|
600
846
|
private async executeSignal(sig: TradeSignal): Promise<void> {
|
|
601
|
-
const venue = sig.venue;
|
|
847
|
+
const venue = sig.venue ? this.normalizeVenueForExecution(sig.venue) ?? sig.venue : sig.venue;
|
|
848
|
+
const signal = venue && venue !== sig.venue
|
|
849
|
+
? { ...sig, venue }
|
|
850
|
+
: sig;
|
|
851
|
+
|
|
852
|
+
if (signal.venue && !this.isVenueConfigured(signal.venue)) {
|
|
853
|
+
getLogger().warn('runtime', `Venue "${signal.venue}" is not enabled in config — signal dropped`, { symbol: signal.symbol });
|
|
854
|
+
this.signal.reportInfo('Signal Dropped', `${signal.symbol}: venue ${signal.venue} is not enabled in this agent config.`);
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
602
857
|
|
|
603
858
|
if (this.paper) {
|
|
604
859
|
// Paper trading
|
|
605
|
-
const trade = this.paper.execute(
|
|
860
|
+
const trade = this.paper.execute(signal, this.market);
|
|
606
861
|
if (trade) {
|
|
607
862
|
// Record realized PnL for daily loss circuit breaker
|
|
608
863
|
this.risk.recordTrade(trade.pnl ?? 0, trade.fee);
|
|
@@ -613,7 +868,7 @@ export class AgentRuntime {
|
|
|
613
868
|
}
|
|
614
869
|
|
|
615
870
|
this.signal.reportTrade({
|
|
616
|
-
...
|
|
871
|
+
...signal,
|
|
617
872
|
price: trade.entryPrice,
|
|
618
873
|
fee: trade.fee,
|
|
619
874
|
venue: trade.venue,
|
|
@@ -626,17 +881,17 @@ export class AgentRuntime {
|
|
|
626
881
|
|
|
627
882
|
// Live trading — route to venue
|
|
628
883
|
if (venue === 'hyperliquid_perp' && this.hlOrders) {
|
|
629
|
-
await this.executeHyperliquidSignal(
|
|
884
|
+
await this.executeHyperliquidSignal(signal);
|
|
630
885
|
} else if (venue === 'polymarket' && this.pmOrders) {
|
|
631
|
-
await this.executePolymarketSignal(
|
|
632
|
-
} else if ((venue === 'uniswap' || venue === 'aerodrome' || venue === '
|
|
633
|
-
await this.executeSpotSignal(
|
|
886
|
+
await this.executePolymarketSignal(signal);
|
|
887
|
+
} else if ((venue === 'uniswap' || venue === 'aerodrome' || venue === 'hyperliquid_spot') && this.spotManager) {
|
|
888
|
+
await this.executeSpotSignal(signal);
|
|
634
889
|
} else if (venue === 'across' && this.bridgeManager) {
|
|
635
|
-
await this.executeBridgeSignal(
|
|
890
|
+
await this.executeBridgeSignal(signal);
|
|
636
891
|
} else if (venue === 'hyperliquid_deposit' && this.hlSigner) {
|
|
637
|
-
await this.executeHyperliquidDeposit(
|
|
892
|
+
await this.executeHyperliquidDeposit(signal);
|
|
638
893
|
} else if (venue === 'hyperliquid_withdraw' && this.hlSigner) {
|
|
639
|
-
await this.executeHyperliquidWithdraw(
|
|
894
|
+
await this.executeHyperliquidWithdraw(signal);
|
|
640
895
|
} else {
|
|
641
896
|
getLogger().warn('runtime', `No executor for venue "${venue}" — signal dropped`, { symbol: sig.symbol });
|
|
642
897
|
}
|
|
@@ -685,14 +940,15 @@ export class AgentRuntime {
|
|
|
685
940
|
if (!result.venueFillId) result.venueFillId = '';
|
|
686
941
|
if (!result.venueTimestamp) result.venueTimestamp = '';
|
|
687
942
|
|
|
688
|
-
|
|
943
|
+
if (result.venue) {
|
|
944
|
+
result.venue = this.normalizeVenueForExecution(result.venue) ?? result.venue;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// Infer venue from the current strategy + enabled venue config if missing
|
|
689
948
|
if (!result.venue) {
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
result.venue = 'polymarket';
|
|
694
|
-
} else if (this.spotManager) {
|
|
695
|
-
result.venue = this.config.venues?.spot?.defaultChain === 'base' ? 'aerodrome' : 'uniswap';
|
|
949
|
+
const candidates = this.getPreferredExecutionVenues().filter((venue) => this.canExecuteVenue(venue));
|
|
950
|
+
if (candidates.length > 0) {
|
|
951
|
+
result.venue = candidates[0];
|
|
696
952
|
}
|
|
697
953
|
}
|
|
698
954
|
|
|
@@ -773,7 +1029,12 @@ export class AgentRuntime {
|
|
|
773
1029
|
if (!this.spotManager) return;
|
|
774
1030
|
|
|
775
1031
|
const chain = sig.chain ?? this.config.venues?.spot?.defaultChain ?? 'base';
|
|
776
|
-
const
|
|
1032
|
+
const routedVenue = this.normalizeVenueForExecution(sig.venue) ?? this.getConfiguredSpotVenue();
|
|
1033
|
+
const dex = routedVenue === 'aerodrome' ? 'aerodrome' : 'uniswap';
|
|
1034
|
+
|
|
1035
|
+
if (sig.venue === 'hyperliquid_spot') {
|
|
1036
|
+
getLogger().info('venue', 'Routing hyperliquid_spot signal through configured spot executor', { routedAs: dex, chain });
|
|
1037
|
+
}
|
|
777
1038
|
|
|
778
1039
|
// For spot, the symbol format is "TOKENIN/TOKENOUT" (e.g., "ETH/USDC")
|
|
779
1040
|
const parts = sig.symbol.split('/');
|
|
@@ -1162,6 +1423,15 @@ export class AgentRuntime {
|
|
|
1162
1423
|
const agent = await res.json() as { config?: Record<string, unknown> };
|
|
1163
1424
|
if (agent.config) {
|
|
1164
1425
|
const cfg = agent.config as Record<string, unknown>;
|
|
1426
|
+
const previousMode = this.mode;
|
|
1427
|
+
|
|
1428
|
+
if (this.tradingInterval) {
|
|
1429
|
+
clearInterval(this.tradingInterval);
|
|
1430
|
+
this.tradingInterval = null;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
await this.waitForCycleCompletion();
|
|
1434
|
+
|
|
1165
1435
|
// Update risk params if present
|
|
1166
1436
|
const risk = cfg.risk as Record<string, unknown> | undefined;
|
|
1167
1437
|
if (risk) {
|
|
@@ -1179,10 +1449,73 @@ export class AgentRuntime {
|
|
|
1179
1449
|
if (typeof risk.tradingIntervalSec === 'number') {
|
|
1180
1450
|
this.config.trading.tradingIntervalMs = risk.tradingIntervalSec * 1000;
|
|
1181
1451
|
}
|
|
1452
|
+
if (typeof risk.minTradeValueUSD === 'number') {
|
|
1453
|
+
this.config.trading.minTradeValueUSD = risk.minTradeValueUSD;
|
|
1454
|
+
updates.minTradeValueUSD = risk.minTradeValueUSD;
|
|
1455
|
+
}
|
|
1182
1456
|
if (Object.keys(updates).length > 0) {
|
|
1183
1457
|
this.risk.updateParams(updates);
|
|
1184
1458
|
}
|
|
1185
1459
|
}
|
|
1460
|
+
|
|
1461
|
+
if (typeof cfg.paperTrading === 'boolean') {
|
|
1462
|
+
this.config.trading.mode = cfg.paperTrading ? 'paper' : 'live';
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
const llm = cfg.llm as Record<string, unknown> | undefined;
|
|
1466
|
+
if (llm) {
|
|
1467
|
+
let llmChanged = false;
|
|
1468
|
+
const nextLlm = { ...this.config.llm };
|
|
1469
|
+
|
|
1470
|
+
if (typeof llm.provider === 'string' && llm.provider !== nextLlm.provider) {
|
|
1471
|
+
nextLlm.provider = llm.provider as RuntimeConfig['llm']['provider'];
|
|
1472
|
+
llmChanged = true;
|
|
1473
|
+
}
|
|
1474
|
+
if (typeof llm.model === 'string' && llm.model !== nextLlm.model) {
|
|
1475
|
+
nextLlm.model = llm.model;
|
|
1476
|
+
llmChanged = true;
|
|
1477
|
+
}
|
|
1478
|
+
if (typeof llm.endpoint === 'string' && llm.endpoint !== nextLlm.endpoint) {
|
|
1479
|
+
nextLlm.endpoint = llm.endpoint;
|
|
1480
|
+
llmChanged = true;
|
|
1481
|
+
}
|
|
1482
|
+
if (typeof llm.temperature === 'number' && llm.temperature !== nextLlm.temperature) {
|
|
1483
|
+
nextLlm.temperature = llm.temperature;
|
|
1484
|
+
llmChanged = true;
|
|
1485
|
+
}
|
|
1486
|
+
if (typeof llm.maxTokens === 'number' && llm.maxTokens !== nextLlm.maxTokens) {
|
|
1487
|
+
nextLlm.maxTokens = llm.maxTokens;
|
|
1488
|
+
llmChanged = true;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
if (llmChanged) {
|
|
1492
|
+
this.config.llm = nextLlm;
|
|
1493
|
+
this.llm = createLLMAdapter(this.config.llm);
|
|
1494
|
+
this.configureLLMAdapter(this.llm);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
if (Array.isArray(cfg.venues)) {
|
|
1499
|
+
const selectedVenues = cfg.venues.filter((venue): venue is string => typeof venue === 'string');
|
|
1500
|
+
this.config.venues = this.buildRuntimeVenuesFromSelection(selectedVenues);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
const nextStrategyConfig = this.extractStrategyConfigFromAgentConfig(cfg);
|
|
1504
|
+
const nextStrategy = await loadStrategy(nextStrategyConfig);
|
|
1505
|
+
this.config.strategy = nextStrategyConfig;
|
|
1506
|
+
this.strategy = nextStrategy;
|
|
1507
|
+
await this.applyExecutionMode();
|
|
1508
|
+
|
|
1509
|
+
if (previousMode === 'trading' || previousMode === 'paper') {
|
|
1510
|
+
this.startTrading();
|
|
1511
|
+
} else if (previousMode === 'paused') {
|
|
1512
|
+
this.modeBeforePause = this.config.trading.mode === 'paper' ? 'paper' : 'trading';
|
|
1513
|
+
this.mode = 'paused';
|
|
1514
|
+
} else {
|
|
1515
|
+
this.mode = 'idle';
|
|
1516
|
+
this.modeBeforePause = null;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1186
1519
|
getLogger().info('runtime', 'Config reloaded from API');
|
|
1187
1520
|
this.sendStatus();
|
|
1188
1521
|
}
|
|
@@ -1359,19 +1692,19 @@ export class AgentRuntime {
|
|
|
1359
1692
|
},
|
|
1360
1693
|
venues: {
|
|
1361
1694
|
hyperliquid: {
|
|
1362
|
-
enabled:
|
|
1695
|
+
enabled: this.config.venues?.hyperliquid_perp?.enabled === true,
|
|
1363
1696
|
trading: !!this.hlOrders && (this.mode === 'trading' || this.mode === 'paused'),
|
|
1364
1697
|
},
|
|
1365
1698
|
polymarket: {
|
|
1366
|
-
enabled:
|
|
1699
|
+
enabled: this.config.venues?.polymarket?.enabled === true,
|
|
1367
1700
|
trading: !!this.pmOrders && (this.mode === 'trading' || this.mode === 'paused'),
|
|
1368
1701
|
},
|
|
1369
1702
|
spot: {
|
|
1370
|
-
enabled:
|
|
1703
|
+
enabled: this.config.venues?.spot?.enabled === true,
|
|
1371
1704
|
trading: !!this.spotManager && (this.mode === 'trading' || this.mode === 'paused'),
|
|
1372
1705
|
},
|
|
1373
1706
|
bridge: {
|
|
1374
|
-
enabled:
|
|
1707
|
+
enabled: this.config.venues?.bridge?.enabled === true,
|
|
1375
1708
|
},
|
|
1376
1709
|
},
|
|
1377
1710
|
};
|