@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/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.1.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
- console.log(`Created ${opts.config}`);
24
- console.log('Edit only the public strategy/venue/risk settings in the file.');
25
- console.log(`Then run: exagent setup --config ${opts.config}`);
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
- console.error('Failed to complete secure local setup:', (err as Error).message);
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
- console.error('Failed to start agent:', (err as Error).message);
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('\nAvailable strategy templates:\n');
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(` ${t.name} — ${t.description}`);
72
- console.log(` Risk: ${t.riskLevel} | Venues: ${t.venues.join(', ') || 'any'}`);
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
- console.error(`API error: ${res.status}`);
101
+ printError(`API error: ${res.status}`);
93
102
  process.exit(1);
94
103
  }
95
104
 
96
- const agent = await res.json();
97
- console.log(JSON.stringify(agent, null, 2));
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
- console.error('Failed to check status:', (err as Error).message);
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
- const SDK_VERSION = '0.1.0';
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
- log.info('strategy', msg);
480
- this.signal.reportInfo('Strategy Log', msg);
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(sig, this.market);
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
- ...sig,
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(sig);
884
+ await this.executeHyperliquidSignal(signal);
630
885
  } else if (venue === 'polymarket' && this.pmOrders) {
631
- await this.executePolymarketSignal(sig);
632
- } else if ((venue === 'uniswap' || venue === 'aerodrome' || venue === 'sushiswap') && this.spotManager) {
633
- await this.executeSpotSignal(sig);
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(sig);
890
+ await this.executeBridgeSignal(signal);
636
891
  } else if (venue === 'hyperliquid_deposit' && this.hlSigner) {
637
- await this.executeHyperliquidDeposit(sig);
892
+ await this.executeHyperliquidDeposit(signal);
638
893
  } else if (venue === 'hyperliquid_withdraw' && this.hlSigner) {
639
- await this.executeHyperliquidWithdraw(sig);
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
- // Infer venue from config if missing
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
- if (this.hlOrders) {
691
- result.venue = 'hyperliquid_perp';
692
- } else if (this.pmOrders) {
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 dex = sig.venue === 'aerodrome' ? 'aerodrome' : 'uniswap';
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: !!this.hlClient,
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: !!this.pmClient,
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: !!this.spotManager,
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: !!this.bridgeManager,
1707
+ enabled: this.config.venues?.bridge?.enabled === true,
1375
1708
  },
1376
1709
  },
1377
1710
  };