@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.
- package/dist/chunk-7UGLJO6W.js +6392 -0
- package/dist/chunk-EHAOPCTJ.js +6406 -0
- package/dist/chunk-FGMXTW5I.js +6540 -0
- package/dist/chunk-IVA2SCSN.js +6756 -0
- package/dist/chunk-JHXCSGPC.js +6352 -0
- package/dist/chunk-V6O4UXVN.js +6345 -0
- package/dist/chunk-ZRAOPQQW.js +6406 -0
- package/dist/cli.js +40 -98
- package/dist/index.d.ts +24 -2
- package/dist/index.js +1 -1
- package/package.json +17 -14
- package/.turbo/turbo-build.log +0 -17
- package/src/bridge/across.ts +0 -240
- package/src/bridge/bridge-manager.ts +0 -87
- package/src/bridge/index.ts +0 -9
- package/src/bridge/types.ts +0 -77
- package/src/chains.ts +0 -105
- package/src/cli.ts +0 -244
- package/src/config.ts +0 -499
- package/src/diagnostics.ts +0 -335
- package/src/index.ts +0 -98
- package/src/llm/anthropic.ts +0 -63
- package/src/llm/base.ts +0 -264
- package/src/llm/deepseek.ts +0 -48
- package/src/llm/google.ts +0 -63
- package/src/llm/groq.ts +0 -48
- package/src/llm/index.ts +0 -42
- package/src/llm/mistral.ts +0 -48
- package/src/llm/ollama.ts +0 -52
- package/src/llm/openai.ts +0 -51
- package/src/llm/together.ts +0 -48
- package/src/llm-providers.ts +0 -100
- package/src/logger.ts +0 -137
- package/src/paper/executor.ts +0 -201
- package/src/paper/index.ts +0 -1
- package/src/perp/client.ts +0 -200
- package/src/perp/index.ts +0 -12
- package/src/perp/msgpack.ts +0 -272
- package/src/perp/orders.ts +0 -234
- package/src/perp/positions.ts +0 -126
- package/src/perp/signer.ts +0 -277
- package/src/perp/types.ts +0 -192
- package/src/perp/websocket.ts +0 -274
- package/src/position-tracker.ts +0 -243
- package/src/prediction/client.ts +0 -281
- package/src/prediction/index.ts +0 -3
- package/src/prediction/order-manager.ts +0 -297
- package/src/prediction/types.ts +0 -151
- package/src/relay.ts +0 -254
- package/src/runtime.ts +0 -1755
- package/src/scrub-secrets.ts +0 -39
- package/src/setup.ts +0 -384
- package/src/signal.ts +0 -212
- package/src/spot/aerodrome.ts +0 -158
- package/src/spot/client.ts +0 -138
- package/src/spot/index.ts +0 -11
- package/src/spot/swap-manager.ts +0 -219
- package/src/spot/types.ts +0 -203
- package/src/spot/uniswap.ts +0 -150
- package/src/store.ts +0 -50
- package/src/strategy/index.ts +0 -2
- package/src/strategy/loader.ts +0 -191
- package/src/strategy/templates.ts +0 -125
- package/src/trading/index.ts +0 -2
- package/src/trading/market.ts +0 -120
- package/src/trading/risk.ts +0 -107
- package/src/ui.ts +0 -75
- package/test-bridge-arb-to-base.mjs +0 -223
- package/test-funded-check.mjs +0 -79
- package/test-funded-phase19.mjs +0 -933
- package/test-hl-deposit-recover.mjs +0 -281
- package/test-hl-withdraw.mjs +0 -372
- package/test-live-signing.mjs +0 -374
- package/test-phase7.mjs +0 -416
- package/test-recover-arb.mjs +0 -206
- package/test-spot-bridge.mjs +0 -248
- package/test-wallet-setup.mjs +0 -126
- 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
|
-
}
|