@exagent/agent 0.3.5 → 0.3.7

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 (78) hide show
  1. package/dist/chunk-7UGLJO6W.js +6392 -0
  2. package/dist/chunk-EHAOPCTJ.js +6406 -0
  3. package/dist/chunk-FGMXTW5I.js +6540 -0
  4. package/dist/chunk-IVA2SCSN.js +6756 -0
  5. package/dist/chunk-JHXCSGPC.js +6352 -0
  6. package/dist/chunk-V6O4UXVN.js +6345 -0
  7. package/dist/chunk-ZRAOPQQW.js +6406 -0
  8. package/dist/cli.js +40 -98
  9. package/dist/index.d.ts +24 -2
  10. package/dist/index.js +1 -1
  11. package/package.json +17 -14
  12. package/.turbo/turbo-build.log +0 -17
  13. package/src/bridge/across.ts +0 -240
  14. package/src/bridge/bridge-manager.ts +0 -87
  15. package/src/bridge/index.ts +0 -9
  16. package/src/bridge/types.ts +0 -77
  17. package/src/chains.ts +0 -105
  18. package/src/cli.ts +0 -244
  19. package/src/config.ts +0 -499
  20. package/src/diagnostics.ts +0 -335
  21. package/src/index.ts +0 -98
  22. package/src/llm/anthropic.ts +0 -63
  23. package/src/llm/base.ts +0 -264
  24. package/src/llm/deepseek.ts +0 -48
  25. package/src/llm/google.ts +0 -63
  26. package/src/llm/groq.ts +0 -48
  27. package/src/llm/index.ts +0 -42
  28. package/src/llm/mistral.ts +0 -48
  29. package/src/llm/ollama.ts +0 -52
  30. package/src/llm/openai.ts +0 -51
  31. package/src/llm/together.ts +0 -48
  32. package/src/llm-providers.ts +0 -100
  33. package/src/logger.ts +0 -137
  34. package/src/paper/executor.ts +0 -201
  35. package/src/paper/index.ts +0 -1
  36. package/src/perp/client.ts +0 -200
  37. package/src/perp/index.ts +0 -12
  38. package/src/perp/msgpack.ts +0 -272
  39. package/src/perp/orders.ts +0 -234
  40. package/src/perp/positions.ts +0 -126
  41. package/src/perp/signer.ts +0 -277
  42. package/src/perp/types.ts +0 -192
  43. package/src/perp/websocket.ts +0 -274
  44. package/src/position-tracker.ts +0 -243
  45. package/src/prediction/client.ts +0 -281
  46. package/src/prediction/index.ts +0 -3
  47. package/src/prediction/order-manager.ts +0 -297
  48. package/src/prediction/types.ts +0 -151
  49. package/src/relay.ts +0 -254
  50. package/src/runtime.ts +0 -1755
  51. package/src/scrub-secrets.ts +0 -39
  52. package/src/setup.ts +0 -384
  53. package/src/signal.ts +0 -212
  54. package/src/spot/aerodrome.ts +0 -158
  55. package/src/spot/client.ts +0 -138
  56. package/src/spot/index.ts +0 -11
  57. package/src/spot/swap-manager.ts +0 -219
  58. package/src/spot/types.ts +0 -203
  59. package/src/spot/uniswap.ts +0 -150
  60. package/src/store.ts +0 -50
  61. package/src/strategy/index.ts +0 -2
  62. package/src/strategy/loader.ts +0 -191
  63. package/src/strategy/templates.ts +0 -125
  64. package/src/trading/index.ts +0 -2
  65. package/src/trading/market.ts +0 -120
  66. package/src/trading/risk.ts +0 -107
  67. package/src/ui.ts +0 -75
  68. package/test-bridge-arb-to-base.mjs +0 -223
  69. package/test-funded-check.mjs +0 -79
  70. package/test-funded-phase19.mjs +0 -933
  71. package/test-hl-deposit-recover.mjs +0 -281
  72. package/test-hl-withdraw.mjs +0 -372
  73. package/test-live-signing.mjs +0 -374
  74. package/test-phase7.mjs +0 -416
  75. package/test-recover-arb.mjs +0 -206
  76. package/test-spot-bridge.mjs +0 -248
  77. package/test-wallet-setup.mjs +0 -126
  78. package/tsconfig.json +0 -8
package/src/runtime.ts DELETED
@@ -1,1755 +0,0 @@
1
- import type {
2
- LLMAdapter,
3
- StrategyFunction,
4
- AgentMode,
5
- AgentStatusPayload,
6
- RelayCommand,
7
- TradeSignal,
8
- } from '@exagent/sdk';
9
- import { createWalletClient, createPublicClient, http, formatUnits } from 'viem';
10
- import { privateKeyToAccount } from 'viem/accounts';
11
- import { arbitrum } from 'viem/chains';
12
- import { getChainConfig, applyRpcOverrides } from './chains.js';
13
- import { ERC20_ABI } from './spot/types.js';
14
- import { RelayClient } from './relay.js';
15
- import { SignalReporter } from './signal.js';
16
- import { FileStore } from './store.js';
17
- import { PositionTracker } from './position-tracker.js';
18
- import { createLLMAdapter } from './llm/index.js';
19
- import { BaseLLMAdapter } from './llm/base.js';
20
- import { loadStrategy } from './strategy/index.js';
21
- import { RiskManager } from './trading/risk.js';
22
- import { MarketDataService } from './trading/market.js';
23
- import { PaperExecutor } from './paper/index.js';
24
- import type { RuntimeConfig } from './config.js';
25
- import { getLogger, configureLogger } from './logger.js';
26
- import type { LogLevel } from './logger.js';
27
- import { DiagnosticsCollector } from './diagnostics.js';
28
- import type { CycleTimings } from './diagnostics.js';
29
- import { scrubSecrets } from './scrub-secrets.js';
30
-
31
- // Hyperliquid
32
- import { HyperliquidClient } from './perp/client.js';
33
- import { HyperliquidSigner } from './perp/signer.js';
34
- import { HyperliquidOrderManager } from './perp/orders.js';
35
- import { HyperliquidPositionManager } from './perp/positions.js';
36
- import { HyperliquidWebSocket } from './perp/websocket.js';
37
- import type { PerpFill, PerpConfig } from './perp/types.js';
38
-
39
- // Polymarket
40
- import { PolymarketClient } from './prediction/client.js';
41
- import { PolymarketOrderManager } from './prediction/order-manager.js';
42
- import { encodePredictionInstrument } from './prediction/types.js';
43
- import type { PredictionConfig } from './prediction/types.js';
44
-
45
- // Spot DEX
46
- import { SpotSwapManager } from './spot/swap-manager.js';
47
- import type { SpotConfig } from './spot/types.js';
48
-
49
- // Bridge
50
- import { BridgeManager } from './bridge/bridge-manager.js';
51
- import type { BridgeConfig } from './bridge/types.js';
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 {}
57
-
58
- /** Number of consecutive cycle failures before switching to idle */
59
- const MAX_CONSECUTIVE_FAILURES = 3;
60
-
61
- function getCycleErrorHint(category: string, msg: string): string | null {
62
- const lower = msg.toLowerCase();
63
- if (category === 'auth' || lower.includes('401') || lower.includes('unauthorized') || lower.includes('invalid api key') || lower.includes('authentication'))
64
- return 'Check your LLM API key — update via npx exagent setup';
65
- if (category === 'network' || lower.includes('econnrefused') || lower.includes('enotfound') || lower.includes('fetch failed') || lower.includes('timeout'))
66
- return 'Network connectivity issue — check your internet connection';
67
- if (category === 'venue' || lower.includes('hyperliquid') || lower.includes('polymarket') || lower.includes('venue'))
68
- return 'Venue API error — the venue may be experiencing issues';
69
- if (category === 'strategy' || lower.includes('invalid') && lower.includes('signal') || lower.includes('strategy'))
70
- return 'Strategy returned invalid output — check your strategy file';
71
- if (lower.includes('rate limit') || lower.includes('429'))
72
- return 'Rate limited — consider increasing tradingIntervalMs';
73
- return null;
74
- }
75
-
76
- export class AgentRuntime {
77
- private config: RuntimeConfig;
78
- private relay: RelayClient;
79
- private signal: SignalReporter;
80
- private store: FileStore;
81
- private positions: PositionTracker;
82
- private llm: LLMAdapter;
83
- private strategy: StrategyFunction | null = null;
84
- private risk: RiskManager;
85
- private market: MarketDataService;
86
- private paper: PaperExecutor | null = null;
87
-
88
- // Venue clients
89
- private hlClient: HyperliquidClient | null = null;
90
- private hlSigner: HyperliquidSigner | null = null;
91
- private hlOrders: HyperliquidOrderManager | null = null;
92
- private hlPositions: HyperliquidPositionManager | null = null;
93
- private hlWs: HyperliquidWebSocket | null = null;
94
- private pmClient: PolymarketClient | null = null;
95
- private pmOrders: PolymarketOrderManager | null = null;
96
- private spotManager: SpotSwapManager | null = null;
97
- private bridgeManager: BridgeManager | null = null;
98
- private walletAddress: string | null = null;
99
-
100
- private mode: AgentMode = 'idle';
101
- private cycleCount = 0;
102
- private lastCycleAt = 0;
103
- private tradingInterval: ReturnType<typeof setInterval> | null = null;
104
- private running = false;
105
- private cycleInProgress = false;
106
- private consecutiveCycleFailures = 0;
107
- /** The mode the agent was in before pausing (for resume) */
108
- private modeBeforePause: AgentMode | null = null;
109
- private diagnostics: DiagnosticsCollector;
110
-
111
- constructor(config: RuntimeConfig) {
112
- this.config = config;
113
-
114
- // Configure structured logger from config
115
- if (config.logging) {
116
- configureLogger({
117
- minLevel: config.logging.level as LogLevel,
118
- json: config.logging.json,
119
- });
120
- }
121
-
122
- // Apply RPC overrides before any chain config usage
123
- if (config.rpcOverrides) {
124
- applyRpcOverrides(config.rpcOverrides);
125
- }
126
-
127
- this.store = new FileStore(`data/${config.agentId}-store.json`);
128
- this.positions = new PositionTracker(this.store);
129
- this.market = new MarketDataService(30000, config.apiUrl);
130
- this.risk = new RiskManager(
131
- {
132
- maxPositionSizeBps: config.trading.maxPositionSizeBps,
133
- maxDailyLossBps: config.trading.maxDailyLossBps,
134
- maxConcurrentPositions: config.trading.maxConcurrentPositions,
135
- maxSlippageBps: config.trading.maxSlippageBps,
136
- minTradeValueUSD: config.trading.minTradeValueUSD,
137
- },
138
- config.trading.initialCapitalUSD,
139
- );
140
-
141
- this.diagnostics = new DiagnosticsCollector();
142
- this.llm = createLLMAdapter(config.llm);
143
- this.configureLLMAdapter(this.llm);
144
-
145
- this.relay = new RelayClient({
146
- url: config.relay.url,
147
- agentId: config.agentId,
148
- token: config.apiToken,
149
- heartbeatIntervalMs: config.relay.heartbeatIntervalMs,
150
- reconnectMaxAttempts: config.relay.reconnectMaxAttempts,
151
- onCommand: (cmd) => this.handleCommand(cmd),
152
- onConnected: () => {
153
- getLogger().info('relay', 'Connected to command center');
154
- this.sendStatus();
155
- },
156
- onDisconnected: () => {
157
- getLogger().warn('relay', 'Disconnected from command center');
158
- },
159
- onReconnected: () => {
160
- getLogger().info('relay', 'Reconnected to command center — flushing signal queue');
161
- this.signal.flushQueue();
162
- this.diagnostics.recordSignalFlush(this.signal.queueSize);
163
- },
164
- });
165
-
166
- // Pass store to SignalReporter for queue persistence
167
- this.signal = new SignalReporter(this.relay, this.store);
168
-
169
- if (config.trading.mode === 'paper') {
170
- this.paper = new PaperExecutor(config.trading.initialCapitalUSD ?? 10000);
171
- }
172
- }
173
-
174
- async start(): Promise<void> {
175
- const log = getLogger();
176
-
177
- log.info('runtime', 'Starting agent', {
178
- agentId: this.config.agentId,
179
- mode: this.config.trading.mode,
180
- provider: this.config.llm.provider,
181
- model: this.config.llm.model,
182
- });
183
-
184
- if (this.config.rpcOverrides && Object.keys(this.config.rpcOverrides).length > 0) {
185
- log.info('runtime', 'RPC overrides applied', { chains: Object.keys(this.config.rpcOverrides) });
186
- }
187
-
188
- // Load strategy
189
- try {
190
- this.strategy = await loadStrategy(this.config.strategy);
191
- log.info('runtime', 'Strategy loaded');
192
- } catch (err) {
193
- log.error('runtime', `Failed to load strategy: ${(err as Error).message}`);
194
- this.strategy = null;
195
- }
196
-
197
- // Initialize venue clients (only in live mode)
198
- if (this.config.trading.mode === 'live') {
199
- await this.initializeVenues();
200
- }
201
-
202
- // Connect to relay
203
- try {
204
- await this.relay.connect();
205
- this.signal.reportInfo('Agent Started', `Agent ${this.config.agentId} connected to command center`);
206
-
207
- // Flush any persisted signals from a previous crash
208
- if (this.signal.queueSize > 0) {
209
- this.signal.flushQueue();
210
- }
211
- } catch (err) {
212
- log.error('runtime', `Failed to connect to relay: ${(err as Error).message}`);
213
- log.info('runtime', 'Will retry in background...');
214
- }
215
-
216
- this.running = true;
217
- this.mode = 'idle';
218
- this.sendStatus();
219
-
220
- // Auto-start trading if configured
221
- if (this.config.trading.mode === 'paper' || this.config.trading.mode === 'live') {
222
- this.startTrading();
223
- }
224
-
225
- // Keep process alive
226
- process.on('SIGTERM', () => this.stop());
227
- process.on('SIGINT', () => this.stop());
228
- }
229
-
230
- async stop(): Promise<void> {
231
- const log = getLogger();
232
- log.info('runtime', 'Stopping agent', { agentId: this.config.agentId });
233
- this.running = false;
234
- this.stopTrading();
235
-
236
- // Disconnect venue WebSockets
237
- if (this.hlWs) {
238
- this.hlWs.disconnect();
239
- }
240
-
241
- this.signal.reportInfo('Agent Stopped', `Agent ${this.config.agentId} shutting down`);
242
- this.relay.disconnect();
243
- log.info('runtime', 'Agent stopped');
244
- process.exit(0);
245
- }
246
-
247
- private configureLLMAdapter(adapter: LLMAdapter): void {
248
- if (this.config.llmBudget?.maxDailyTokens && adapter instanceof BaseLLMAdapter) {
249
- adapter.setMaxDailyTokens(this.config.llmBudget.maxDailyTokens);
250
- }
251
-
252
- if (adapter instanceof BaseLLMAdapter) {
253
- adapter.setCallRecordCallback((record) => {
254
- this.diagnostics.recordLLMCall(record);
255
- });
256
- }
257
- }
258
-
259
- private teardownVenues(): void {
260
- if (this.hlWs) {
261
- this.hlWs.disconnect();
262
- }
263
-
264
- this.hlWs = null;
265
- this.hlClient = null;
266
- this.hlSigner = null;
267
- this.hlOrders = null;
268
- this.hlPositions = null;
269
- this.pmClient = null;
270
- this.pmOrders = null;
271
- this.spotManager = null;
272
- this.bridgeManager = null;
273
- this.walletAddress = null;
274
- }
275
-
276
- private async waitForCycleCompletion(timeoutMs: number = 30000): Promise<void> {
277
- let waitedMs = 0;
278
- while (this.cycleInProgress && waitedMs < timeoutMs) {
279
- await new Promise((resolve) => setTimeout(resolve, 100));
280
- waitedMs += 100;
281
- }
282
- }
283
-
284
- private getConfiguredSpotVenue(): 'aerodrome' | 'uniswap' {
285
- return this.config.venues?.spot?.defaultChain === 'base' ? 'aerodrome' : 'uniswap';
286
- }
287
-
288
- private normalizeVenueForExecution(venue?: string): string | undefined {
289
- if (!venue) return undefined;
290
-
291
- if (venue === 'hyperliquid_spot') {
292
- return this.getConfiguredSpotVenue();
293
- }
294
-
295
- if (venue === 'sushiswap') {
296
- return 'uniswap';
297
- }
298
-
299
- return venue;
300
- }
301
-
302
- private isVenueConfigured(venue: string): boolean {
303
- const normalized = this.normalizeVenueForExecution(venue) ?? venue;
304
-
305
- switch (normalized) {
306
- case 'hyperliquid_perp':
307
- case 'hyperliquid_deposit':
308
- case 'hyperliquid_withdraw':
309
- return this.config.venues?.hyperliquid_perp?.enabled === true;
310
- case 'polymarket':
311
- return this.config.venues?.polymarket?.enabled === true;
312
- case 'uniswap':
313
- case 'aerodrome':
314
- return this.config.venues?.spot?.enabled === true;
315
- case 'across':
316
- return this.config.venues?.bridge?.enabled === true;
317
- default:
318
- return false;
319
- }
320
- }
321
-
322
- private canExecuteVenue(venue: string): boolean {
323
- const normalized = this.normalizeVenueForExecution(venue) ?? venue;
324
- if (!this.isVenueConfigured(normalized)) {
325
- return false;
326
- }
327
-
328
- if (this.paper) {
329
- return true;
330
- }
331
-
332
- switch (normalized) {
333
- case 'hyperliquid_perp':
334
- case 'hyperliquid_deposit':
335
- case 'hyperliquid_withdraw':
336
- return !!this.hlOrders || !!this.hlSigner;
337
- case 'polymarket':
338
- return !!this.pmOrders;
339
- case 'uniswap':
340
- case 'aerodrome':
341
- return !!this.spotManager;
342
- case 'across':
343
- return !!this.bridgeManager;
344
- default:
345
- return false;
346
- }
347
- }
348
-
349
- private getPreferredExecutionVenues(): string[] {
350
- const preferred = [
351
- ...(this.config.strategy.venues ?? []),
352
- ...(this.config.strategy.prompt?.venues ?? []),
353
- ];
354
-
355
- const normalized = preferred
356
- .map((venue) => this.normalizeVenueForExecution(venue))
357
- .filter((venue): venue is string => Boolean(venue));
358
-
359
- const fallback: string[] = [];
360
- if (this.config.venues?.hyperliquid_perp?.enabled) fallback.push('hyperliquid_perp');
361
- if (this.config.venues?.polymarket?.enabled) fallback.push('polymarket');
362
- if (this.config.venues?.spot?.enabled) fallback.push(this.getConfiguredSpotVenue());
363
- if (this.config.venues?.bridge?.enabled) fallback.push('across');
364
-
365
- return [...new Set([...normalized, ...fallback])];
366
- }
367
-
368
- private buildRuntimeVenuesFromSelection(selectedVenues: string[]): NonNullable<RuntimeConfig['venues']> {
369
- const selected = new Set(selectedVenues);
370
- const current = this.config.venues ?? {};
371
- const spotEnabled = selectedVenues.some((venue) => ['hyperliquid_spot', 'uniswap', 'aerodrome', 'sushiswap'].includes(venue));
372
- const multiChainSpot = selected.has('uniswap') || selected.has('sushiswap');
373
- const defaultSpotChains = multiChainSpot
374
- ? ['base', 'ethereum', 'arbitrum', 'polygon']
375
- : ['base'];
376
-
377
- return {
378
- hyperliquid_perp: {
379
- enabled: selected.has('hyperliquid_perp'),
380
- apiUrl: current.hyperliquid_perp?.apiUrl ?? 'https://api.hyperliquid.xyz',
381
- wsUrl: current.hyperliquid_perp?.wsUrl ?? 'wss://api.hyperliquid.xyz/ws',
382
- maxLeverage: current.hyperliquid_perp?.maxLeverage ?? 10,
383
- maxNotionalUSD: current.hyperliquid_perp?.maxNotionalUSD ?? 50000,
384
- allowedInstruments: current.hyperliquid_perp?.allowedInstruments,
385
- },
386
- polymarket: {
387
- enabled: selected.has('polymarket'),
388
- clobApiUrl: current.polymarket?.clobApiUrl ?? 'https://clob.polymarket.com',
389
- gammaApiUrl: current.polymarket?.gammaApiUrl ?? 'https://gamma-api.polymarket.com',
390
- maxNotionalUSD: current.polymarket?.maxNotionalUSD ?? 1000,
391
- maxTotalExposureUSD: current.polymarket?.maxTotalExposureUSD ?? 5000,
392
- allowedCategories: current.polymarket?.allowedCategories,
393
- },
394
- spot: {
395
- enabled: spotEnabled,
396
- chains: current.spot?.chains?.length ? Array.from(new Set([...defaultSpotChains, ...current.spot.chains])) : defaultSpotChains,
397
- defaultChain: current.spot?.defaultChain ?? 'base',
398
- maxSlippageBps: current.spot?.maxSlippageBps ?? this.config.trading.maxSlippageBps,
399
- maxSwapValueUSD: current.spot?.maxSwapValueUSD ?? 10000,
400
- },
401
- bridge: {
402
- enabled: selected.has('across'),
403
- defaultBridge: current.bridge?.defaultBridge ?? 'across',
404
- maxBridgeValueUSD: current.bridge?.maxBridgeValueUSD ?? 10000,
405
- fillTimeoutMs: current.bridge?.fillTimeoutMs ?? 300000,
406
- pollIntervalMs: current.bridge?.pollIntervalMs ?? 2000,
407
- },
408
- };
409
- }
410
-
411
- private buildStrategyFallbackPrompt(strategy: {
412
- name?: string;
413
- description?: string;
414
- category?: string;
415
- venues?: string[];
416
- }): string {
417
- const name = strategy.name?.trim() || 'Dashboard Strategy';
418
- const description = strategy.description?.trim();
419
- const category = strategy.category?.trim();
420
- const venues = strategy.venues?.length ? strategy.venues.join(', ') : 'any';
421
-
422
- return [
423
- `You are the "${name}" trading agent.`,
424
- category ? `Strategy category: ${category}.` : null,
425
- description
426
- ? `Strategy description: ${description}`
427
- : 'Strategy description: follow the owner-configured strategy exactly and do not improvise outside it.',
428
- `Allowed venues: ${venues}.`,
429
- 'Stay inside the owner-configured scope. If the setup is unclear or the trade does not fit, return no trades.',
430
- 'Return ONLY a JSON array of trade signals.',
431
- ].filter((line): line is string => Boolean(line)).join('\n');
432
- }
433
-
434
- private extractStrategyConfigFromAgentConfig(cfg: Record<string, unknown>): RuntimeConfig['strategy'] {
435
- const rawStrategy = cfg.strategy as Record<string, unknown> | undefined;
436
- if (!rawStrategy) {
437
- return { template: 'hold' };
438
- }
439
-
440
- const venues = Array.isArray(rawStrategy.venues)
441
- ? rawStrategy.venues.filter((venue): venue is string => typeof venue === 'string')
442
- : undefined;
443
-
444
- if (typeof rawStrategy.file === 'string' && rawStrategy.file.trim()) {
445
- return { file: rawStrategy.file, venues };
446
- }
447
-
448
- if (typeof rawStrategy.code === 'string' && rawStrategy.code.trim()) {
449
- return { code: rawStrategy.code, venues };
450
- }
451
-
452
- if (typeof rawStrategy.systemPrompt === 'string' && rawStrategy.systemPrompt.trim()) {
453
- return {
454
- venues,
455
- prompt: {
456
- name: typeof rawStrategy.name === 'string' ? rawStrategy.name : undefined,
457
- systemPrompt: rawStrategy.systemPrompt,
458
- venues,
459
- },
460
- };
461
- }
462
-
463
- if (typeof rawStrategy.template === 'string' && rawStrategy.template.trim()) {
464
- return {
465
- template: rawStrategy.template,
466
- venues,
467
- };
468
- }
469
-
470
- return {
471
- venues,
472
- prompt: {
473
- name: typeof rawStrategy.name === 'string' ? rawStrategy.name : undefined,
474
- systemPrompt: this.buildStrategyFallbackPrompt({
475
- name: typeof rawStrategy.name === 'string' ? rawStrategy.name : undefined,
476
- description: typeof rawStrategy.description === 'string' ? rawStrategy.description : undefined,
477
- category: typeof rawStrategy.category === 'string' ? rawStrategy.category : undefined,
478
- venues,
479
- }),
480
- venues,
481
- },
482
- };
483
- }
484
-
485
- private async applyExecutionMode(): Promise<void> {
486
- if (this.config.trading.mode === 'paper') {
487
- this.teardownVenues();
488
- if (!this.paper) {
489
- this.paper = new PaperExecutor(this.config.trading.initialCapitalUSD ?? 10000);
490
- }
491
- return;
492
- }
493
-
494
- this.paper = null;
495
- this.teardownVenues();
496
- await this.initializeVenues();
497
- }
498
-
499
- // ── VENUE INITIALIZATION ───────────────────────────────────
500
-
501
- private async initializeVenues(): Promise<void> {
502
- const log = getLogger();
503
- const privateKey = this.config.wallet?.privateKey;
504
- if (!privateKey) {
505
- log.warn('runtime', 'No wallet configured — live trading disabled');
506
- return;
507
- }
508
-
509
- // Derive wallet address from private key
510
- const walletAccount = privateKeyToAccount(privateKey as `0x${string}`);
511
- this.walletAddress = walletAccount.address;
512
-
513
- // Initialize Hyperliquid
514
- const hlConfig = this.config.venues?.hyperliquid_perp;
515
- if (hlConfig?.enabled) {
516
- try {
517
- const account = walletAccount;
518
- const walletClient = createWalletClient({
519
- account,
520
- chain: arbitrum,
521
- transport: http(),
522
- });
523
-
524
- const perpConfig: PerpConfig = {
525
- enabled: true,
526
- apiUrl: hlConfig.apiUrl,
527
- wsUrl: hlConfig.wsUrl,
528
- maxLeverage: hlConfig.maxLeverage,
529
- maxNotionalUSD: hlConfig.maxNotionalUSD,
530
- allowedInstruments: hlConfig.allowedInstruments,
531
- };
532
-
533
- this.hlClient = new HyperliquidClient(perpConfig);
534
- this.hlSigner = new HyperliquidSigner(walletClient);
535
- this.hlOrders = new HyperliquidOrderManager(this.hlClient, this.hlSigner, perpConfig);
536
- this.hlPositions = new HyperliquidPositionManager(this.hlClient, account.address);
537
- this.hlWs = new HyperliquidWebSocket(perpConfig, account.address, this.hlClient);
538
-
539
- // Fetch metadata (caches asset indices)
540
- await this.hlClient.getMeta();
541
-
542
- // Register fill callback
543
- this.hlWs.onFillReceived((fill: PerpFill) => {
544
- this.handleHyperliquidFill(fill);
545
- });
546
-
547
- this.hlWs.onLiquidationDetected((instrument, size) => {
548
- this.signal.reportError('Liquidation Detected', `${instrument}: ${size} liquidated`);
549
- });
550
-
551
- // Connect WebSocket
552
- await this.hlWs.connect();
553
-
554
- log.info('venue', 'Hyperliquid initialized', { address: account.address });
555
- } catch (err) {
556
- log.error('venue', `Failed to initialize Hyperliquid: ${(err as Error).message}`);
557
- }
558
- }
559
-
560
- // Initialize Polymarket
561
- const pmConfig = this.config.venues?.polymarket;
562
- if (pmConfig?.enabled) {
563
- try {
564
- const predConfig: PredictionConfig = {
565
- enabled: true,
566
- clobApiUrl: pmConfig.clobApiUrl,
567
- gammaApiUrl: pmConfig.gammaApiUrl,
568
- maxNotionalUSD: pmConfig.maxNotionalUSD,
569
- maxTotalExposureUSD: pmConfig.maxTotalExposureUSD,
570
- allowedCategories: pmConfig.allowedCategories,
571
- };
572
-
573
- this.pmClient = new PolymarketClient(privateKey, predConfig);
574
- await this.pmClient.initialize();
575
- this.pmOrders = new PolymarketOrderManager(this.pmClient, predConfig);
576
-
577
- log.info('venue', 'Polymarket initialized');
578
- } catch (err) {
579
- log.error('venue', `Failed to initialize Polymarket: ${(err as Error).message}`);
580
- }
581
- }
582
-
583
- // Initialize Spot DEX
584
- const spotConfig = this.config.venues?.spot;
585
- if (spotConfig?.enabled) {
586
- try {
587
- const cfg: SpotConfig = {
588
- enabled: true,
589
- chains: spotConfig.chains,
590
- defaultChain: spotConfig.defaultChain,
591
- maxSlippageBps: spotConfig.maxSlippageBps,
592
- maxSwapValueUSD: spotConfig.maxSwapValueUSD,
593
- };
594
- this.spotManager = new SpotSwapManager(privateKey, cfg);
595
- log.info('venue', 'Spot DEX initialized', { chains: spotConfig.chains });
596
- } catch (err) {
597
- log.error('venue', `Failed to initialize Spot DEX: ${(err as Error).message}`);
598
- }
599
- }
600
-
601
- // Initialize Bridge
602
- const bridgeConfig = this.config.venues?.bridge;
603
- if (bridgeConfig?.enabled) {
604
- try {
605
- if (!this.spotManager) {
606
- // Bridge needs a SpotDEXClient for approvals — create a minimal spot manager
607
- const minimalSpotConfig: SpotConfig = {
608
- enabled: true,
609
- chains: ['base', 'ethereum', 'arbitrum', 'polygon'],
610
- defaultChain: 'base',
611
- maxSlippageBps: 50,
612
- maxSwapValueUSD: 10_000,
613
- };
614
- this.spotManager = new SpotSwapManager(privateKey, minimalSpotConfig);
615
- }
616
-
617
- const cfg: BridgeConfig = {
618
- enabled: true,
619
- defaultBridge: bridgeConfig.defaultBridge,
620
- maxBridgeValueUSD: bridgeConfig.maxBridgeValueUSD,
621
- fillTimeoutMs: bridgeConfig.fillTimeoutMs,
622
- pollIntervalMs: bridgeConfig.pollIntervalMs,
623
- };
624
- this.bridgeManager = new BridgeManager(this.spotManager.client, cfg);
625
- log.info('venue', 'Bridge initialized');
626
- } catch (err) {
627
- log.error('venue', `Failed to initialize Bridge: ${(err as Error).message}`);
628
- }
629
- }
630
- }
631
-
632
- // ── TRADING LOOP ───────────────────────────────────────────
633
-
634
- private startTrading(): void {
635
- if (this.tradingInterval) return;
636
-
637
- this.mode = this.config.trading.mode === 'paper' ? 'paper' : 'trading';
638
- this.modeBeforePause = null;
639
- this.consecutiveCycleFailures = 0;
640
- getLogger().info('runtime', 'Trading started', { mode: this.mode, intervalMs: this.config.trading.tradingIntervalMs });
641
- this.sendStatus();
642
- this.signal.reportInfo('Trading Started', `Mode: ${this.mode}`);
643
-
644
- this.runCycle();
645
-
646
- this.tradingInterval = setInterval(() => {
647
- this.runCycle();
648
- }, this.config.trading.tradingIntervalMs);
649
- }
650
-
651
- private stopTrading(): void {
652
- if (this.tradingInterval) {
653
- clearInterval(this.tradingInterval);
654
- this.tradingInterval = null;
655
- }
656
- this.mode = 'idle';
657
- this.modeBeforePause = null;
658
- this.sendStatus();
659
- getLogger().info('runtime', 'Trading stopped');
660
- }
661
-
662
- private pauseTrading(): void {
663
- if (this.tradingInterval) {
664
- clearInterval(this.tradingInterval);
665
- this.tradingInterval = null;
666
- }
667
- this.modeBeforePause = this.mode;
668
- this.mode = 'paused';
669
- this.sendStatus();
670
- getLogger().info('runtime', 'Trading paused — heartbeats continue, no new cycles');
671
- }
672
-
673
- private resumeTrading(): void {
674
- if (this.mode !== 'paused') return;
675
-
676
- this.mode = this.modeBeforePause ?? (this.config.trading.mode === 'paper' ? 'paper' : 'trading');
677
- this.modeBeforePause = null;
678
- this.consecutiveCycleFailures = 0;
679
- getLogger().info('runtime', 'Trading resumed', { mode: this.mode });
680
- this.sendStatus();
681
- this.signal.reportInfo('Trading Resumed', `Mode: ${this.mode}`);
682
-
683
- this.runCycle();
684
-
685
- this.tradingInterval = setInterval(() => {
686
- this.runCycle();
687
- }, this.config.trading.tradingIntervalMs);
688
- }
689
-
690
- private async runCycle(): Promise<void> {
691
- const log = getLogger();
692
-
693
- if (!this.running || !this.strategy) return;
694
-
695
- // Prevent overlapping cycles — if a strategy call takes longer than tradingIntervalMs
696
- if (this.cycleInProgress) {
697
- log.warn('cycle', 'Skipping cycle — previous cycle still running');
698
- return;
699
- }
700
-
701
- this.cycleInProgress = true;
702
- this.cycleCount++;
703
- this.lastCycleAt = Date.now();
704
-
705
- const cycleStart = Date.now();
706
- const timings: CycleTimings = {
707
- totalMs: 0,
708
- priceRefreshMs: 0,
709
- strategyMs: 0,
710
- riskFilterMs: 0,
711
- executionMs: 0,
712
- };
713
- let signalsGenerated = 0;
714
- let signalsFiltered = 0;
715
- let tradesExecuted = 0;
716
-
717
- try {
718
- // Phase 1: Refresh market prices
719
- const priceStart = Date.now();
720
- const positionSymbols = this.positions.getPositions().map(p => p.token);
721
- const commonSymbols = ['BTC', 'ETH', 'SOL', 'USDC'];
722
- const allSymbols = [...new Set([...positionSymbols, ...commonSymbols])];
723
- await this.market.refreshPrices(allSymbols);
724
- timings.priceRefreshMs = Date.now() - priceStart;
725
-
726
- // Get position summary with fresh prices
727
- const prices = this.market.getPrices();
728
- const positionSummary = this.positions.getSummary(prices);
729
-
730
- // Phase 2: Run strategy
731
- const strategyStart = Date.now();
732
- const rawSignals = await this.strategy({
733
- llm: this.llm,
734
- market: this.market,
735
- position: positionSummary,
736
- store: this.store,
737
- config: this.config.trading,
738
- log: (msg: string) => {
739
- const safe = scrubSecrets(msg);
740
- log.info('strategy', safe);
741
- this.signal.reportInfo('Strategy Log', safe);
742
- },
743
- });
744
- timings.strategyMs = Date.now() - strategyStart;
745
-
746
- // Cycle succeeded — reset consecutive failure counter
747
- this.consecutiveCycleFailures = 0;
748
-
749
- if (!Array.isArray(rawSignals) || rawSignals.length === 0) {
750
- await this.pollPredictionFills();
751
- timings.totalMs = Date.now() - cycleStart;
752
- this.diagnostics.recordCycle({
753
- cycleNumber: this.cycleCount,
754
- startedAt: cycleStart,
755
- timings,
756
- signalsGenerated: 0,
757
- signalsFiltered: 0,
758
- tradesExecuted: 0,
759
- success: true,
760
- });
761
- log.debug('cycle', 'Completed (no signals)', {
762
- cycle: this.cycleCount,
763
- totalMs: timings.totalMs,
764
- priceMs: timings.priceRefreshMs,
765
- strategyMs: timings.strategyMs,
766
- });
767
- this.sendStatus();
768
- return;
769
- }
770
-
771
- signalsGenerated = rawSignals.length;
772
-
773
- // Convert strategy outputs to executable signals
774
- const signals = rawSignals.map(sig => this.convertToExecutableSignal(sig, prices));
775
-
776
- // Phase 3: Risk filtering
777
- const riskStart = Date.now();
778
- const filtered = this.risk.filterSignals(signals, this.market, positionSummary.openPositions.length);
779
- timings.riskFilterMs = Date.now() - riskStart;
780
- signalsFiltered = filtered.length;
781
-
782
- // Phase 4: Execute trades
783
- const execStart = Date.now();
784
- for (const sig of filtered) {
785
- try {
786
- await this.executeSignal(sig);
787
- tradesExecuted++;
788
- } catch (err) {
789
- const errMsg = (err as Error).message;
790
- log.error('venue', `Trade execution error: ${errMsg}`, { symbol: sig.symbol, venue: sig.venue });
791
- this.signal.reportError('Trade Error', errMsg);
792
- this.diagnostics.recordError(err as Error, 'venue');
793
- }
794
- }
795
- timings.executionMs = Date.now() - execStart;
796
-
797
- // Poll Polymarket fills
798
- await this.pollPredictionFills();
799
-
800
- timings.totalMs = Date.now() - cycleStart;
801
- this.diagnostics.recordCycle({
802
- cycleNumber: this.cycleCount,
803
- startedAt: cycleStart,
804
- timings,
805
- signalsGenerated,
806
- signalsFiltered,
807
- tradesExecuted,
808
- success: true,
809
- });
810
-
811
- log.info('cycle', 'Completed', {
812
- cycle: this.cycleCount,
813
- totalMs: timings.totalMs,
814
- priceMs: timings.priceRefreshMs,
815
- strategyMs: timings.strategyMs,
816
- riskMs: timings.riskFilterMs,
817
- execMs: timings.executionMs,
818
- signals: signalsGenerated,
819
- filtered: signalsFiltered,
820
- executed: tradesExecuted,
821
- });
822
- } catch (err) {
823
- const errMsg = (err as Error).message;
824
- timings.totalMs = Date.now() - cycleStart;
825
-
826
- const categorized = this.diagnostics.recordError(err as Error);
827
- const hint = getCycleErrorHint(categorized.category, errMsg);
828
- log.error('cycle', `Cycle error: ${errMsg}${hint ? ` — ${hint}` : ''}`, {
829
- cycle: this.cycleCount,
830
- totalMs: timings.totalMs,
831
- category: categorized.category,
832
- });
833
- this.signal.reportError('Cycle Error', `${errMsg}${hint ? ` — ${hint}` : ''}`);
834
-
835
- this.diagnostics.recordCycle({
836
- cycleNumber: this.cycleCount,
837
- startedAt: cycleStart,
838
- timings,
839
- signalsGenerated,
840
- signalsFiltered,
841
- tradesExecuted,
842
- success: false,
843
- error: errMsg,
844
- });
845
-
846
- // Only switch to idle on persistent failures (3 consecutive cycle failures)
847
- this.consecutiveCycleFailures++;
848
- if (this.consecutiveCycleFailures >= MAX_CONSECUTIVE_FAILURES) {
849
- log.error('runtime', `${MAX_CONSECUTIVE_FAILURES} consecutive cycle failures — switching to idle`);
850
- this.signal.reportError('Trading Halted', `${MAX_CONSECUTIVE_FAILURES} consecutive cycle failures. Last: ${errMsg}`);
851
- this.stopTrading();
852
- }
853
- } finally {
854
- this.cycleInProgress = false;
855
- }
856
-
857
- this.sendStatus();
858
- }
859
-
860
- // ── SIGNAL EXECUTION ───────────────────────────────────────
861
-
862
- private async executeSignal(sig: TradeSignal): Promise<void> {
863
- const venue = sig.venue ? this.normalizeVenueForExecution(sig.venue) ?? sig.venue : sig.venue;
864
- const signal = venue && venue !== sig.venue
865
- ? { ...sig, venue }
866
- : sig;
867
-
868
- if (signal.venue && !this.isVenueConfigured(signal.venue)) {
869
- getLogger().warn('runtime', `Venue "${signal.venue}" is not enabled in config — signal dropped`, { symbol: signal.symbol });
870
- this.signal.reportInfo('Signal Dropped', `${signal.symbol}: venue ${signal.venue} is not enabled in this agent config.`);
871
- return;
872
- }
873
-
874
- if (this.paper) {
875
- // Paper trading
876
- const trade = this.paper.execute(signal, this.market);
877
- if (trade) {
878
- // Record realized PnL for daily loss circuit breaker
879
- this.risk.recordTrade(trade.pnl ?? 0, trade.fee);
880
- if (this.risk.isDailyLossLimitHit()) {
881
- getLogger().warn('risk', 'Daily loss limit hit (paper) — stopping trading');
882
- this.signal.reportError('Daily Loss Limit', `Daily PnL: $${this.risk.getDailyPnL().toFixed(2)} exceeds limit`);
883
- this.stopTrading();
884
- }
885
-
886
- this.signal.reportTrade({
887
- ...signal,
888
- price: trade.entryPrice,
889
- fee: trade.fee,
890
- venue: trade.venue,
891
- venueFillId: trade.id,
892
- venueTimestamp: new Date(trade.timestamp).toISOString(),
893
- });
894
- }
895
- return;
896
- }
897
-
898
- // Live trading — route to venue
899
- if (venue === 'hyperliquid_perp' && this.hlOrders) {
900
- await this.executeHyperliquidSignal(signal);
901
- } else if (venue === 'polymarket' && this.pmOrders) {
902
- await this.executePolymarketSignal(signal);
903
- } else if ((venue === 'uniswap' || venue === 'aerodrome' || venue === 'hyperliquid_spot') && this.spotManager) {
904
- await this.executeSpotSignal(signal);
905
- } else if (venue === 'across' && this.bridgeManager) {
906
- await this.executeBridgeSignal(signal);
907
- } else if (venue === 'hyperliquid_deposit' && this.hlSigner) {
908
- await this.executeHyperliquidDeposit(signal);
909
- } else if (venue === 'hyperliquid_withdraw' && this.hlSigner) {
910
- await this.executeHyperliquidWithdraw(signal);
911
- } else {
912
- getLogger().warn('runtime', `No executor for venue "${venue}" — signal dropped`, { symbol: sig.symbol });
913
- }
914
- }
915
-
916
- /**
917
- * Convert a raw strategy output to an executable TradeSignal.
918
- * LLM templates return { symbol, side, confidence, reasoning } but execution
919
- * needs { price, size, fee, venue, venueFillId, venueTimestamp }.
920
- */
921
- private convertToExecutableSignal(sig: TradeSignal, prices: Record<string, number>): TradeSignal {
922
- const result = { ...sig };
923
-
924
- // Fill in price from market data if missing
925
- if (!result.price || result.price <= 0) {
926
- const symbol = result.symbol || '';
927
- const marketPrice = prices[symbol.toUpperCase()] || prices[symbol] || this.market.getPrice(symbol);
928
- if (marketPrice && marketPrice > 0) {
929
- result.price = marketPrice;
930
- } else {
931
- getLogger().warn('runtime', `No price for ${symbol} — signal will fail risk checks`);
932
- result.price = 0;
933
- }
934
- }
935
-
936
- // Compute size if missing — use a fixed fraction of capital (2% default)
937
- if (!result.size || result.size <= 0) {
938
- if (result.price > 0) {
939
- const capital = this.config.trading.initialCapitalUSD ?? 10000;
940
- // Default to 2% of capital (200 bps) as a conservative position size
941
- const defaultPositionBps = 200;
942
- const usdSize = (defaultPositionBps / 10000) * capital;
943
- result.size = usdSize / result.price;
944
- } else {
945
- // No price, no size — risk filter will reject
946
- result.size = 0;
947
- }
948
- }
949
-
950
- // Default fee to 0 (filled by venue on execution)
951
- if (result.fee === undefined || result.fee === null) {
952
- result.fee = 0;
953
- }
954
-
955
- // Default venue fill fields (populated after execution)
956
- if (!result.venueFillId) result.venueFillId = '';
957
- if (!result.venueTimestamp) result.venueTimestamp = '';
958
-
959
- if (result.venue) {
960
- result.venue = this.normalizeVenueForExecution(result.venue) ?? result.venue;
961
- }
962
-
963
- // Infer venue from the current strategy + enabled venue config if missing
964
- if (!result.venue) {
965
- const candidates = this.getPreferredExecutionVenues().filter((venue) => this.canExecuteVenue(venue));
966
- if (candidates.length > 0) {
967
- result.venue = candidates[0];
968
- }
969
- }
970
-
971
- return result;
972
- }
973
-
974
- private async executeHyperliquidSignal(sig: TradeSignal): Promise<void> {
975
- if (!this.hlOrders) return;
976
-
977
- // Map unified TradeSignal to PerpTradeSignal
978
- const action = sig.side === 'long'
979
- ? 'open_long' as const
980
- : sig.side === 'short'
981
- ? 'open_short' as const
982
- : sig.side === 'buy'
983
- ? 'open_long' as const
984
- : 'close_long' as const;
985
-
986
- const result = await this.hlOrders.placeOrder({
987
- action,
988
- instrument: sig.symbol,
989
- size: sig.size,
990
- price: sig.price,
991
- leverage: sig.leverage ?? 1,
992
- orderType: (sig.orderType as 'limit' | 'market') ?? 'market',
993
- reduceOnly: action.startsWith('close'),
994
- confidence: sig.confidence ?? 1.0,
995
- reasoning: sig.reasoning,
996
- });
997
-
998
- if (result.success && result.status === 'filled') {
999
- getLogger().info('venue', 'HL order filled', { symbol: sig.symbol, price: result.avgPrice });
1000
- } else if (result.success && result.status === 'resting') {
1001
- getLogger().info('venue', 'HL order resting', { symbol: sig.symbol, orderId: result.orderId });
1002
- } else {
1003
- getLogger().warn('venue', `HL order failed: ${result.error}`, { symbol: sig.symbol });
1004
- this.signal.reportError('Perp Order Failed', `${sig.symbol}: ${result.error}`);
1005
- this.diagnostics.recordError(result.error ?? 'Unknown error', 'venue');
1006
- }
1007
- }
1008
-
1009
- private async executePolymarketSignal(sig: TradeSignal): Promise<void> {
1010
- if (!this.pmOrders) return;
1011
-
1012
- // Determine outcome: "no" if orderType === 'no', otherwise "yes" (default)
1013
- // Convention: signal.orderType carries 'yes'|'no' for Polymarket (defaults to 'yes')
1014
- const isNo = sig.orderType === 'no';
1015
- const outcomeIndex = isNo ? 1 : 0;
1016
-
1017
- // Map unified TradeSignal to PredictionTradeSignal action
1018
- const isBuy = sig.side === 'buy' || sig.side === 'long';
1019
- const action = isBuy
1020
- ? (isNo ? 'buy_no' as const : 'buy_yes' as const)
1021
- : (isNo ? 'sell_no' as const : 'sell_yes' as const);
1022
-
1023
- const result = await this.pmOrders.executeSignal({
1024
- action,
1025
- marketConditionId: sig.symbol,
1026
- marketQuestion: sig.reasoning ?? sig.symbol,
1027
- outcomeIndex,
1028
- amount: sig.size,
1029
- limitPrice: sig.price,
1030
- orderType: 'limit',
1031
- confidence: sig.confidence ?? 1.0,
1032
- reasoning: sig.reasoning,
1033
- });
1034
-
1035
- if (result.success) {
1036
- getLogger().info('venue', 'PM order placed', { symbol: sig.symbol });
1037
- } else {
1038
- getLogger().warn('venue', `PM order failed: ${result.error}`, { symbol: sig.symbol });
1039
- this.signal.reportError('Prediction Order Failed', `${sig.symbol}: ${result.error}`);
1040
- this.diagnostics.recordError(result.error ?? 'Unknown error', 'venue');
1041
- }
1042
- }
1043
-
1044
- private async executeSpotSignal(sig: TradeSignal): Promise<void> {
1045
- if (!this.spotManager) return;
1046
-
1047
- const chain = sig.chain ?? this.config.venues?.spot?.defaultChain ?? 'base';
1048
- const routedVenue = this.normalizeVenueForExecution(sig.venue) ?? this.getConfiguredSpotVenue();
1049
- const dex = routedVenue === 'aerodrome' ? 'aerodrome' : 'uniswap';
1050
-
1051
- if (sig.venue === 'hyperliquid_spot') {
1052
- getLogger().info('venue', 'Routing hyperliquid_spot signal through configured spot executor', { routedAs: dex, chain });
1053
- }
1054
-
1055
- // For spot, the symbol format is "TOKENIN/TOKENOUT" (e.g., "ETH/USDC")
1056
- const parts = sig.symbol.split('/');
1057
- const isBuy = sig.side === 'buy' || sig.side === 'long';
1058
-
1059
- // Buy ETH/USDC means: swap USDC → ETH. Sell ETH/USDC means: swap ETH → USDC.
1060
- const tokenIn = isBuy ? (parts[1] ?? 'USDC') : (parts[0] ?? sig.symbol);
1061
- const tokenOut = isBuy ? (parts[0] ?? sig.symbol) : (parts[1] ?? 'USDC');
1062
-
1063
- // Convert size to token amount (size is in quote currency for buys, base for sells)
1064
- const tokenInAddress = this.spotManager.resolveToken(tokenIn, chain);
1065
- const decimals = await this.spotManager.client.getDecimals(tokenInAddress, chain);
1066
- const amountIn = BigInt(Math.floor(sig.size * 10 ** decimals));
1067
-
1068
- const result = await this.spotManager.executeSwap({
1069
- tokenIn,
1070
- tokenOut,
1071
- amountIn,
1072
- chain,
1073
- dex,
1074
- });
1075
-
1076
- if (result.success) {
1077
- const tokenOutSymbol = await this.spotManager.client.getSymbol(result.tokenOut, chain);
1078
- const tokenInSymbol = await this.spotManager.client.getSymbol(result.tokenIn, chain);
1079
- const tokenOutDecimals = await this.spotManager.client.getDecimals(result.tokenOut, chain);
1080
- const amountOutFloat = Number(result.amountOut) / 10 ** tokenOutDecimals;
1081
- const amountInFloat = Number(result.amountIn) / 10 ** decimals;
1082
-
1083
- const tradeSignal: TradeSignal = {
1084
- venue: result.dex,
1085
- chain,
1086
- symbol: `${parts[0] ?? sig.symbol}`,
1087
- side: sig.side,
1088
- size: isBuy ? amountOutFloat : amountInFloat,
1089
- price: result.effectivePrice,
1090
- fee: Number(result.gasCost) / 1e18,
1091
- venueFillId: result.txHash,
1092
- venueTimestamp: new Date().toISOString(),
1093
- };
1094
-
1095
- // Update local position tracker and record PnL for daily loss limit
1096
- const baseToken = parts[0] ?? sig.symbol;
1097
- let realizedPnL = 0;
1098
- if (isBuy) {
1099
- realizedPnL = this.positions.recordBuy(
1100
- baseToken, amountOutFloat, result.effectivePrice,
1101
- tradeSignal.fee, result.dex, chain, result.txHash,
1102
- );
1103
- } else {
1104
- realizedPnL = this.positions.recordSell(
1105
- baseToken, amountInFloat, result.effectivePrice,
1106
- tradeSignal.fee, result.dex, chain, result.txHash,
1107
- );
1108
- }
1109
-
1110
- this.risk.recordTrade(realizedPnL, tradeSignal.fee);
1111
- if (this.risk.isDailyLossLimitHit()) {
1112
- getLogger().warn('risk', 'Daily loss limit hit — stopping trading');
1113
- this.signal.reportError('Daily Loss Limit', `Daily PnL: $${this.risk.getDailyPnL().toFixed(2)} exceeds limit`);
1114
- this.stopTrading();
1115
- }
1116
-
1117
- this.signal.reportSpotFill(tradeSignal);
1118
- getLogger().info('venue', 'Spot swap filled', { from: tokenInSymbol, to: tokenOutSymbol, dex: result.dex, chain });
1119
- } else {
1120
- getLogger().warn('venue', `Spot swap failed: ${result.error}`, { symbol: sig.symbol });
1121
- this.signal.reportError('Spot Swap Failed', `${sig.symbol}: ${result.error}`);
1122
- }
1123
- }
1124
-
1125
- private async executeBridgeSignal(sig: TradeSignal): Promise<void> {
1126
- if (!this.bridgeManager || !this.spotManager) return;
1127
-
1128
- // Bridge signal format: symbol = token symbol, chain = destination chain
1129
- // sig.chain is the destination; we need to figure out the source chain
1130
- // Convention: signal.reasoning or orderType can encode "fromChain:toChain"
1131
- const toChain = sig.chain ?? 'arbitrum';
1132
- const fromChain = sig.orderType ?? 'base'; // Use orderType as fromChain hint
1133
-
1134
- const tokenAddress = this.spotManager.resolveToken(sig.symbol, fromChain);
1135
- const decimals = await this.spotManager.client.getDecimals(tokenAddress, fromChain);
1136
- const amount = BigInt(Math.floor(sig.size * 10 ** decimals));
1137
-
1138
- const result = await this.bridgeManager.bridge({
1139
- token: tokenAddress,
1140
- amount,
1141
- fromChain,
1142
- toChain,
1143
- });
1144
-
1145
- if (result.success) {
1146
- const tradeSignal: TradeSignal = {
1147
- venue: 'across',
1148
- chain: toChain,
1149
- symbol: sig.symbol,
1150
- side: 'buy',
1151
- size: sig.size,
1152
- price: sig.price || 1,
1153
- fee: Number(result.fee) / 10 ** decimals,
1154
- venueFillId: result.depositTxHash,
1155
- venueTimestamp: new Date().toISOString(),
1156
- };
1157
-
1158
- this.signal.reportBridgeFill(tradeSignal);
1159
- getLogger().info('bridge', 'Bridge completed', { from: fromChain, to: toChain, bridge: 'across' });
1160
- } else {
1161
- getLogger().warn('bridge', `Bridge failed: ${result.error}`, { symbol: sig.symbol });
1162
- this.signal.reportError('Bridge Failed', `${sig.symbol} ${fromChain}→${toChain}: ${result.error}`);
1163
- }
1164
- }
1165
-
1166
- // ── HYPERLIQUID DEPOSIT / WITHDRAW ────────────────────────
1167
-
1168
- /** Hyperliquid bridge contract on Arbitrum — accepts plain USDC transfers */
1169
- private static readonly HL_BRIDGE_CONTRACT: `0x${string}` = '0x2Df1c51E09aECF9cacB7bc98cB1742757f163dF7';
1170
-
1171
- /**
1172
- * Deposit USDC into Hyperliquid by transferring to the bridge contract on Arbitrum.
1173
- * Signal format: venue='hyperliquid_deposit', size=amount in USDC, chain is ignored (always Arbitrum).
1174
- * The wallet must already have USDC + ETH for gas on Arbitrum.
1175
- */
1176
- private async executeHyperliquidDeposit(sig: TradeSignal): Promise<void> {
1177
- if (!this.spotManager) {
1178
- this.signal.reportError('HL Deposit Failed', 'Spot manager not initialized (needed for chain clients)');
1179
- return;
1180
- }
1181
-
1182
- const arbConfig = getChainConfig('arbitrum');
1183
- if (!arbConfig) {
1184
- this.signal.reportError('HL Deposit Failed', 'Arbitrum chain config not found');
1185
- return;
1186
- }
1187
-
1188
- const usdcAddress = arbConfig.usdcAddress;
1189
- const amount = BigInt(Math.floor(sig.size * 1e6)); // USDC has 6 decimals
1190
-
1191
- // Pre-check: gas balance on Arbitrum
1192
- const MIN_GAS_WEI = 100_000_000_000_000n; // 0.0001 ETH
1193
- try {
1194
- const gasBalance = await this.spotManager.client.getNativeBalance('arbitrum');
1195
- if (gasBalance < MIN_GAS_WEI) {
1196
- this.signal.reportError('HL Deposit Failed', `Insufficient ETH for gas on Arbitrum. Balance: ${formatUnits(gasBalance, 18)} ETH. Need at least 0.0001 ETH.`);
1197
- return;
1198
- }
1199
- } catch {
1200
- // proceed — let the transfer itself fail with a clearer error
1201
- }
1202
-
1203
- // Pre-check: USDC balance on Arbitrum
1204
- try {
1205
- const usdcBalance = await this.spotManager.client.getBalance(usdcAddress, 'arbitrum');
1206
- if (usdcBalance < amount) {
1207
- this.signal.reportError('HL Deposit Failed', `Insufficient USDC on Arbitrum. Have ${formatUnits(usdcBalance, 6)}, need ${formatUnits(amount, 6)}. Bridge USDC to Arbitrum first.`);
1208
- return;
1209
- }
1210
- } catch (err) {
1211
- this.signal.reportError('HL Deposit Failed', `Could not check USDC balance: ${(err as Error).message}`);
1212
- return;
1213
- }
1214
-
1215
- // Deposit = plain ERC20 transfer of USDC to the HL bridge contract
1216
- try {
1217
- const { publicClient, walletClient } = this.spotManager.client.getClients('arbitrum');
1218
- const hash = await walletClient.writeContract({
1219
- address: usdcAddress,
1220
- abi: ERC20_ABI,
1221
- functionName: 'transfer',
1222
- args: [AgentRuntime.HL_BRIDGE_CONTRACT, amount],
1223
- });
1224
-
1225
- const receipt = await publicClient.waitForTransactionReceipt({ hash });
1226
- if (receipt.status === 'reverted') {
1227
- this.signal.reportError('HL Deposit Failed', `Transfer reverted: ${hash}`);
1228
- return;
1229
- }
1230
-
1231
- getLogger().info('venue', 'HL deposit completed', { amount: formatUnits(amount, 6), tx: hash });
1232
- this.signal.reportInfo('HL Deposit', `Deposited ${formatUnits(amount, 6)} USDC to Hyperliquid`);
1233
- } catch (err) {
1234
- getLogger().error('venue', `HL deposit failed: ${(err as Error).message}`);
1235
- this.signal.reportError('HL Deposit Failed', (err as Error).message);
1236
- }
1237
- }
1238
-
1239
- /**
1240
- * Withdraw USDC from Hyperliquid to the wallet on Arbitrum.
1241
- * Signal format: venue='hyperliquid_withdraw', size=amount in USDC.
1242
- * Uses the withdraw3 EIP-712 signing scheme.
1243
- */
1244
- private async executeHyperliquidWithdraw(sig: TradeSignal): Promise<void> {
1245
- if (!this.hlSigner || !this.hlClient) {
1246
- this.signal.reportError('HL Withdraw Failed', 'Hyperliquid signer/client not initialized');
1247
- return;
1248
- }
1249
-
1250
- const destination = this.hlSigner.getAddress();
1251
- const amount = sig.size.toString();
1252
-
1253
- try {
1254
- const { action, signature, nonce } = await this.hlSigner.signWithdraw(destination, amount);
1255
-
1256
- const apiUrl = this.config.venues?.hyperliquid_perp?.apiUrl ?? 'https://api.hyperliquid.xyz';
1257
- const resp = await fetch(`${apiUrl}/exchange`, {
1258
- method: 'POST',
1259
- headers: { 'Content-Type': 'application/json' },
1260
- body: JSON.stringify({ action, nonce, signature, vaultAddress: null }),
1261
- });
1262
-
1263
- if (!resp.ok) {
1264
- const text = await resp.text();
1265
- throw new Error(`HL Exchange API error: ${resp.status} ${text}`);
1266
- }
1267
-
1268
- const data = await resp.json() as { status: string; response?: { type: string } };
1269
- if (data.status !== 'ok') {
1270
- throw new Error(`HL withdraw3 failed: ${JSON.stringify(data)}`);
1271
- }
1272
-
1273
- getLogger().info('venue', 'HL withdrawal completed', { amount, destination });
1274
- this.signal.reportInfo('HL Withdrawal', `Withdrew ${amount} USDC from Hyperliquid to ${destination}`);
1275
- } catch (err) {
1276
- getLogger().error('venue', `HL withdrawal failed: ${(err as Error).message}`);
1277
- this.signal.reportError('HL Withdraw Failed', (err as Error).message);
1278
- }
1279
- }
1280
-
1281
- // ── FILL HANDLING ──────────────────────────────────────────
1282
-
1283
- private handleHyperliquidFill(fill: PerpFill): void {
1284
- const tradeSignal: TradeSignal = {
1285
- venue: 'hyperliquid_perp',
1286
- symbol: fill.coin,
1287
- side: fill.side === 'B' ? 'long' : 'short',
1288
- size: parseFloat(fill.sz),
1289
- price: parseFloat(fill.px),
1290
- fee: parseFloat(fill.fee),
1291
- venueFillId: fill.hash,
1292
- venueTimestamp: new Date(fill.time).toISOString(),
1293
- orderType: fill.isMaker ? 'limit' : 'market',
1294
- };
1295
-
1296
- // Update local position tracker and record PnL for daily loss limit
1297
- const action = fill.side === 'B' ? 'buy' as const : 'sell' as const;
1298
- const fee = parseFloat(fill.fee);
1299
- let realizedPnL = 0;
1300
- if (action === 'buy') {
1301
- realizedPnL = this.positions.recordBuy(
1302
- fill.coin, parseFloat(fill.sz), parseFloat(fill.px),
1303
- fee, 'hyperliquid_perp', undefined, fill.hash,
1304
- );
1305
- } else {
1306
- realizedPnL = this.positions.recordSell(
1307
- fill.coin, parseFloat(fill.sz), parseFloat(fill.px),
1308
- fee, 'hyperliquid_perp', undefined, fill.hash,
1309
- );
1310
- }
1311
-
1312
- // Feed realized PnL to risk manager for daily loss circuit breaker
1313
- this.risk.recordTrade(realizedPnL, fee);
1314
- if (this.risk.isDailyLossLimitHit()) {
1315
- getLogger().warn('risk', 'Daily loss limit hit — stopping trading');
1316
- this.diagnostics.recordError('Daily loss limit hit', 'risk');
1317
- this.signal.reportError('Daily Loss Limit', `Daily PnL: $${this.risk.getDailyPnL().toFixed(2)} exceeds limit of -$${this.risk.getDailyLossLimit().toFixed(2)}`);
1318
- this.stopTrading();
1319
- }
1320
-
1321
- // Report to command center
1322
- this.signal.reportPerpFill(tradeSignal);
1323
- }
1324
-
1325
- private async pollPredictionFills(): Promise<void> {
1326
- if (!this.pmOrders) return;
1327
-
1328
- try {
1329
- const newFills = await this.pmOrders.pollNewFills();
1330
- for (const fill of newFills) {
1331
- const tradeSignal: TradeSignal = {
1332
- venue: 'polymarket',
1333
- chain: 'polygon',
1334
- symbol: encodePredictionInstrument(fill.marketConditionId, fill.outcomeIndex),
1335
- side: fill.side === 'BUY' ? 'buy' : 'sell',
1336
- size: parseFloat(fill.size),
1337
- price: parseFloat(fill.price),
1338
- fee: parseFloat(fill.fee),
1339
- venueFillId: fill.tradeId,
1340
- venueTimestamp: new Date(fill.timestamp).toISOString(),
1341
- };
1342
-
1343
- // Update position tracker and daily loss limit
1344
- const fillFee = parseFloat(fill.fee);
1345
- let realizedPnL = 0;
1346
- if (fill.side === 'BUY') {
1347
- realizedPnL = this.positions.recordBuy(
1348
- tradeSignal.symbol, parseFloat(fill.size), parseFloat(fill.price),
1349
- fillFee, 'polymarket', 'polygon', fill.tradeId,
1350
- );
1351
- } else {
1352
- realizedPnL = this.positions.recordSell(
1353
- tradeSignal.symbol, parseFloat(fill.size), parseFloat(fill.price),
1354
- fillFee, 'polymarket', 'polygon', fill.tradeId,
1355
- );
1356
- }
1357
- this.risk.recordTrade(realizedPnL, fillFee);
1358
- if (this.risk.isDailyLossLimitHit()) {
1359
- getLogger().warn('risk', 'Daily loss limit hit — stopping trading');
1360
- this.signal.reportError('Daily Loss Limit', `Daily PnL: $${this.risk.getDailyPnL().toFixed(2)} exceeds limit`);
1361
- this.stopTrading();
1362
- }
1363
-
1364
- this.signal.reportPredictionFill(tradeSignal);
1365
- }
1366
- } catch (err) {
1367
- // Non-critical — log and continue
1368
- getLogger().warn('venue', `Prediction fill poll error: ${(err as Error).message}`);
1369
- }
1370
- }
1371
-
1372
- // ── COMMANDS ───────────────────────────────────────────────
1373
-
1374
- private handleCommand(command: RelayCommand): void {
1375
- getLogger().info('runtime', 'Command received', { type: command.type, id: command.id });
1376
-
1377
- switch (command.type) {
1378
- case 'start_trading':
1379
- if (this.mode === 'paused') {
1380
- this.resumeTrading();
1381
- this.relay.sendCommandAck(command.id, true, 'Trading resumed from paused state');
1382
- } else {
1383
- this.startTrading();
1384
- this.relay.sendCommandAck(command.id, true, 'Trading started');
1385
- }
1386
- break;
1387
- case 'stop_trading':
1388
- this.stopTrading();
1389
- this.relay.sendCommandAck(command.id, true, 'Trading stopped');
1390
- break;
1391
- case 'pause_trading':
1392
- this.pauseTrading();
1393
- this.relay.sendCommandAck(command.id, true, 'Trading paused — heartbeats continue, no new cycles');
1394
- break;
1395
- case 'update_risk_params':
1396
- if (command.params) {
1397
- this.risk.updateParams(command.params as Record<string, number>);
1398
- this.sendStatus();
1399
- this.relay.sendCommandAck(command.id, true, 'Risk params updated');
1400
- } else {
1401
- this.relay.sendCommandAck(command.id, false, 'No params provided');
1402
- }
1403
- break;
1404
- case 'get_status':
1405
- this.sendStatus();
1406
- this.relay.sendCommandAck(command.id, true);
1407
- break;
1408
- case 'reload_config':
1409
- this.reloadConfig(command);
1410
- break;
1411
- case 'close_all_positions':
1412
- this.closeAllPositions(command);
1413
- break;
1414
- case 'update_strategy':
1415
- this.updateStrategy(command);
1416
- break;
1417
- case 'shutdown':
1418
- this.relay.sendCommandAck(command.id, true, 'Agent shutting down');
1419
- // Give the ack time to send before exiting
1420
- setTimeout(() => {
1421
- this.stop();
1422
- }, 500);
1423
- break;
1424
- default:
1425
- getLogger().warn('runtime', `Unknown command: ${command.type}`);
1426
- this.relay.sendCommandAck(command.id, false, `Unknown command: ${command.type}`);
1427
- }
1428
- }
1429
-
1430
- private async reloadConfig(command: RelayCommand): Promise<void> {
1431
- try {
1432
- const apiBase = this.config.apiUrl || this.config.relay.url.replace(/\/ws\/.*/, '');
1433
- const res = await fetch(`${apiBase}/v1/agents/${this.config.agentId}`, {
1434
- headers: { 'Authorization': `Bearer ${this.config.apiToken}` },
1435
- });
1436
- if (!res.ok) {
1437
- throw new Error(`API returned ${res.status}`);
1438
- }
1439
- const agent = await res.json() as { config?: Record<string, unknown> };
1440
- if (agent.config) {
1441
- const cfg = agent.config as Record<string, unknown>;
1442
- const previousMode = this.mode;
1443
-
1444
- if (this.tradingInterval) {
1445
- clearInterval(this.tradingInterval);
1446
- this.tradingInterval = null;
1447
- }
1448
-
1449
- await this.waitForCycleCompletion();
1450
-
1451
- // Update risk params if present
1452
- const risk = cfg.risk as Record<string, unknown> | undefined;
1453
- if (risk) {
1454
- const updates: Record<string, number> = {};
1455
- if (typeof risk.maxPositionSize === 'number') updates.maxPositionSizeBps = risk.maxPositionSize * 100;
1456
- if (typeof risk.maxPositionSizePct === 'number') updates.maxPositionSizeBps = risk.maxPositionSizePct * 100;
1457
- if (typeof risk.maxDailyLoss === 'number') updates.maxDailyLossBps = risk.maxDailyLoss * 100;
1458
- if (typeof risk.maxDailyLossPct === 'number') updates.maxDailyLossBps = risk.maxDailyLossPct * 100;
1459
- if (typeof risk.maxConcurrentPositions === 'number') updates.maxConcurrentPositions = risk.maxConcurrentPositions;
1460
- if (typeof risk.slippageTolerance === 'number') updates.maxSlippageBps = risk.slippageTolerance * 100;
1461
- if (typeof risk.maxSlippagePct === 'number') updates.maxSlippageBps = risk.maxSlippagePct * 100;
1462
- if (typeof risk.tradingInterval === 'number') {
1463
- this.config.trading.tradingIntervalMs = risk.tradingInterval * 1000;
1464
- }
1465
- if (typeof risk.tradingIntervalSec === 'number') {
1466
- this.config.trading.tradingIntervalMs = risk.tradingIntervalSec * 1000;
1467
- }
1468
- if (typeof risk.minTradeValueUSD === 'number') {
1469
- this.config.trading.minTradeValueUSD = risk.minTradeValueUSD;
1470
- updates.minTradeValueUSD = risk.minTradeValueUSD;
1471
- }
1472
- if (Object.keys(updates).length > 0) {
1473
- this.risk.updateParams(updates);
1474
- }
1475
- }
1476
-
1477
- if (typeof cfg.paperTrading === 'boolean') {
1478
- this.config.trading.mode = cfg.paperTrading ? 'paper' : 'live';
1479
- }
1480
-
1481
- const llm = cfg.llm as Record<string, unknown> | undefined;
1482
- if (llm) {
1483
- let llmChanged = false;
1484
- const nextLlm = { ...this.config.llm };
1485
-
1486
- if (typeof llm.provider === 'string' && llm.provider !== nextLlm.provider) {
1487
- nextLlm.provider = llm.provider as RuntimeConfig['llm']['provider'];
1488
- llmChanged = true;
1489
- }
1490
- if (typeof llm.model === 'string' && llm.model !== nextLlm.model) {
1491
- nextLlm.model = llm.model;
1492
- llmChanged = true;
1493
- }
1494
- if (typeof llm.endpoint === 'string' && llm.endpoint !== nextLlm.endpoint) {
1495
- nextLlm.endpoint = llm.endpoint;
1496
- llmChanged = true;
1497
- }
1498
- if (typeof llm.temperature === 'number' && llm.temperature !== nextLlm.temperature) {
1499
- nextLlm.temperature = llm.temperature;
1500
- llmChanged = true;
1501
- }
1502
- if (typeof llm.maxTokens === 'number' && llm.maxTokens !== nextLlm.maxTokens) {
1503
- nextLlm.maxTokens = llm.maxTokens;
1504
- llmChanged = true;
1505
- }
1506
-
1507
- if (llmChanged) {
1508
- this.config.llm = nextLlm;
1509
- this.llm = createLLMAdapter(this.config.llm);
1510
- this.configureLLMAdapter(this.llm);
1511
- }
1512
- }
1513
-
1514
- if (Array.isArray(cfg.venues)) {
1515
- const selectedVenues = cfg.venues.filter((venue): venue is string => typeof venue === 'string');
1516
- this.config.venues = this.buildRuntimeVenuesFromSelection(selectedVenues);
1517
- }
1518
-
1519
- const nextStrategyConfig = this.extractStrategyConfigFromAgentConfig(cfg);
1520
- const nextStrategy = await loadStrategy(nextStrategyConfig);
1521
- this.config.strategy = nextStrategyConfig;
1522
- this.strategy = nextStrategy;
1523
- await this.applyExecutionMode();
1524
-
1525
- if (previousMode === 'trading' || previousMode === 'paper') {
1526
- this.startTrading();
1527
- } else if (previousMode === 'paused') {
1528
- this.modeBeforePause = this.config.trading.mode === 'paper' ? 'paper' : 'trading';
1529
- this.mode = 'paused';
1530
- } else {
1531
- this.mode = 'idle';
1532
- this.modeBeforePause = null;
1533
- }
1534
-
1535
- getLogger().info('runtime', 'Config reloaded from API');
1536
- this.sendStatus();
1537
- }
1538
- this.relay.sendCommandAck(command.id, true, 'Config reloaded');
1539
- } catch (err) {
1540
- getLogger().error('runtime', `Config reload failed: ${(err as Error).message}`);
1541
- this.relay.sendCommandAck(command.id, false, (err as Error).message);
1542
- }
1543
- }
1544
-
1545
- private async closeAllPositions(command: RelayCommand): Promise<void> {
1546
- const log = getLogger();
1547
- log.info('runtime', 'Emergency close all positions');
1548
-
1549
- // Stop trading first to prevent new positions
1550
- this.stopTrading();
1551
-
1552
- const positions = this.positions.getPositions();
1553
- if (positions.length === 0) {
1554
- this.relay.sendCommandAck(command.id, true, 'No open positions');
1555
- this.sendStatus();
1556
- return;
1557
- }
1558
-
1559
- const results: { token: string; venue: string; success: boolean; error?: string }[] = [];
1560
-
1561
- for (const pos of positions) {
1562
- try {
1563
- if (pos.venue?.startsWith('hyperliquid_perp') && this.hlOrders) {
1564
- const result = await this.hlOrders.closePosition(pos.token, pos.quantity);
1565
- results.push({ token: pos.token, venue: pos.venue, success: result.success, error: result.error });
1566
- } else if (pos.venue === 'polymarket' && this.pmOrders) {
1567
- // Sell shares at market to close
1568
- const result = await this.pmOrders.executeSignal({
1569
- action: pos.quantity > 0 ? 'sell_yes' : 'sell_no',
1570
- marketConditionId: pos.token,
1571
- marketQuestion: '',
1572
- outcomeIndex: 0,
1573
- amount: Math.abs(pos.quantity),
1574
- limitPrice: 0.01,
1575
- confidence: 1.0,
1576
- orderType: 'market',
1577
- });
1578
- results.push({ token: pos.token, venue: 'polymarket', success: result.success, error: result.error });
1579
- } else {
1580
- // Spot positions or unknown venues — skip
1581
- results.push({ token: pos.token, venue: pos.venue || 'spot', success: false, error: 'Spot positions not auto-closable' });
1582
- }
1583
- } catch (err) {
1584
- results.push({ token: pos.token, venue: pos.venue || 'unknown', success: false, error: (err as Error).message });
1585
- }
1586
- }
1587
-
1588
- const closed = results.filter(r => r.success).length;
1589
- const failed = results.filter(r => !r.success && r.error !== 'Spot positions not auto-closable').length;
1590
- const msg = `Closed ${closed}/${positions.length} positions${failed > 0 ? ` (${failed} failed)` : ''}`;
1591
-
1592
- log.info('runtime', msg, { results });
1593
- this.signal.reportInfo('Emergency Close', msg);
1594
- this.relay.sendCommandAck(command.id, failed === 0, msg);
1595
- this.sendStatus();
1596
- }
1597
-
1598
- private async updateStrategy(command: RelayCommand): Promise<void> {
1599
- const log = getLogger();
1600
- const params = command.params as { template?: string; file?: string } | undefined;
1601
-
1602
- if (!params || (!params.template && !params.file)) {
1603
- this.relay.sendCommandAck(command.id, false, 'No strategy specified (need template or file)');
1604
- return;
1605
- }
1606
-
1607
- try {
1608
- const oldName = this.config.strategy?.template || this.config.strategy?.file || 'unknown';
1609
-
1610
- // Pause trading cycle if running
1611
- const wasTrading = this.mode === 'trading' || this.mode === 'paper';
1612
- if (wasTrading && this.tradingInterval) {
1613
- clearInterval(this.tradingInterval);
1614
- this.tradingInterval = null;
1615
- }
1616
-
1617
- // Wait for any in-progress cycle to complete
1618
- let waitMs = 0;
1619
- while (this.cycleInProgress && waitMs < 30000) {
1620
- await new Promise(resolve => setTimeout(resolve, 100));
1621
- waitMs += 100;
1622
- }
1623
-
1624
- // Load new strategy
1625
- const newStrategy = await loadStrategy({
1626
- file: params.file,
1627
- template: params.template,
1628
- });
1629
-
1630
- // Swap
1631
- this.strategy = newStrategy;
1632
- const newName = params.template || params.file || 'unknown';
1633
-
1634
- // Update config reference
1635
- if (params.template) {
1636
- this.config.strategy = { ...this.config.strategy, template: params.template };
1637
- }
1638
- if (params.file) {
1639
- this.config.strategy = { ...this.config.strategy, file: params.file };
1640
- }
1641
-
1642
- // Resume trading if it was running
1643
- if (wasTrading) {
1644
- this.tradingInterval = setInterval(() => {
1645
- this.runCycle();
1646
- }, this.config.trading.tradingIntervalMs);
1647
- }
1648
-
1649
- const msg = `Strategy swapped: ${oldName} → ${newName}`;
1650
- log.info('runtime', msg);
1651
- this.signal.reportInfo('Strategy Updated', msg);
1652
- this.relay.sendCommandAck(command.id, true, msg);
1653
- this.sendStatus();
1654
- } catch (err) {
1655
- const msg = `Strategy swap failed: ${(err as Error).message}`;
1656
- log.error('runtime', msg);
1657
- this.relay.sendCommandAck(command.id, false, msg);
1658
- }
1659
- }
1660
-
1661
- // ── STATUS ─────────────────────────────────────────────────
1662
-
1663
- private sendStatus(): void {
1664
- // Compute unrealized PnL from current market prices
1665
- const prices = this.market.getPrices();
1666
- const positionSummary = this.positions.getSummary(prices);
1667
-
1668
- // Update paper equity with current market prices
1669
- if (this.paper) {
1670
- this.paper.recordEquityWithPrices(prices);
1671
- }
1672
-
1673
- // Get LLM usage stats
1674
- const llmUsage = this.llm instanceof BaseLLMAdapter
1675
- ? this.llm.getUsageStats()
1676
- : null;
1677
-
1678
- const status: AgentStatusPayload = {
1679
- mode: this.mode,
1680
- agentId: this.config.agentId,
1681
- walletAddress: this.walletAddress || undefined,
1682
- sdkVersion: SDK_VERSION,
1683
- cycleCount: this.cycleCount,
1684
- lastCycleAt: this.lastCycleAt,
1685
- tradingIntervalMs: this.config.trading.tradingIntervalMs,
1686
- llm: {
1687
- provider: this.config.llm.provider,
1688
- model: this.config.llm.model || 'default',
1689
- ...(llmUsage ? {
1690
- dailyTokens: llmUsage.dailyInputTokens + llmUsage.dailyOutputTokens,
1691
- totalTokens: llmUsage.totalInputTokens + llmUsage.totalOutputTokens,
1692
- dailyCalls: llmUsage.dailyCalls,
1693
- totalCalls: llmUsage.totalCalls,
1694
- dailyBudgetExceeded: this.llm instanceof BaseLLMAdapter
1695
- ? this.llm.isDailyBudgetExceeded()
1696
- : false,
1697
- } : {}),
1698
- },
1699
- risk: {
1700
- dailyPnL: this.risk.getDailyPnL(),
1701
- dailyLossLimit: this.risk.getDailyLossLimit(),
1702
- isLimitHit: this.risk.isDailyLossLimitHit(),
1703
- },
1704
- positions: {
1705
- openPositions: positionSummary.openPositions.length,
1706
- totalUnrealizedPnL: positionSummary.totalUnrealizedPnL,
1707
- totalRealizedPnL: positionSummary.totalRealizedPnL,
1708
- },
1709
- venues: {
1710
- hyperliquid: {
1711
- enabled: this.config.venues?.hyperliquid_perp?.enabled === true,
1712
- trading: !!this.hlOrders && (this.mode === 'trading' || this.mode === 'paused'),
1713
- },
1714
- polymarket: {
1715
- enabled: this.config.venues?.polymarket?.enabled === true,
1716
- trading: !!this.pmOrders && (this.mode === 'trading' || this.mode === 'paused'),
1717
- },
1718
- spot: {
1719
- enabled: this.config.venues?.spot?.enabled === true,
1720
- trading: !!this.spotManager && (this.mode === 'trading' || this.mode === 'paused'),
1721
- },
1722
- bridge: {
1723
- enabled: this.config.venues?.bridge?.enabled === true,
1724
- },
1725
- },
1726
- };
1727
-
1728
- if (this.paper) {
1729
- const metrics = this.paper.getMetrics();
1730
- status.paper = {
1731
- active: true,
1732
- simulatedValue: this.paper.getEquity(),
1733
- simulatedPnLPct: metrics.totalReturn * 100,
1734
- };
1735
- status.portfolioValue = this.paper.getEquity();
1736
- }
1737
-
1738
- // Diagnostics snapshot
1739
- const health = this.diagnostics.getHealthSnapshot(this.signal.queueSize);
1740
- const lastCycle = this.diagnostics.getLastCycle();
1741
- status.diagnostics = {
1742
- uptime: health.uptime,
1743
- uptimeHuman: health.uptimeHuman,
1744
- memoryMB: health.memoryMB,
1745
- cycleStats: health.cycleStats,
1746
- lastCycleTimings: lastCycle?.timings,
1747
- errorCounts: health.errorCounts,
1748
- recentErrors: health.recentErrors,
1749
- llmStats: health.llmStats,
1750
- signalQueue: health.signalQueue,
1751
- };
1752
-
1753
- this.relay.sendHeartbeat(status);
1754
- }
1755
- }