@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
@@ -1,39 +0,0 @@
1
- /**
2
- * Secret scrubbing utility — defense-in-depth protection against LLM responses
3
- * or log messages accidentally containing API keys, private keys, or tokens.
4
- *
5
- * Applied to:
6
- * - LLM response content before parsing (strategy/loader.ts)
7
- * - Strategy log output (runtime.ts context.log)
8
- */
9
-
10
- const SECRET_PATTERNS: Array<{ pattern: RegExp; label: string }> = [
11
- // OpenAI API keys
12
- { pattern: /sk-[a-zA-Z0-9]{20,}/g, label: '[REDACTED:openai-key]' },
13
- // Anthropic API keys
14
- { pattern: /sk-ant-[a-zA-Z0-9_-]{20,}/g, label: '[REDACTED:anthropic-key]' },
15
- // Google API keys
16
- { pattern: /AIza[a-zA-Z0-9_-]{35}/g, label: '[REDACTED:google-key]' },
17
- // Private keys (64 hex chars after 0x)
18
- { pattern: /0x[a-fA-F0-9]{64}/g, label: '[REDACTED:private-key]' },
19
- // Agent tokens
20
- { pattern: /exg_[a-fA-F0-9]{64}/g, label: '[REDACTED:agent-token]' },
21
- // Bootstrap tokens
22
- { pattern: /exb_[a-fA-F0-9]{64}/g, label: '[REDACTED:bootstrap-token]' },
23
- // Generic long bearer tokens (base64-ish, 40+ chars)
24
- { pattern: /Bearer\s+[A-Za-z0-9_-]{40,}/g, label: '[REDACTED:bearer-token]' },
25
- ];
26
-
27
- /**
28
- * Scrub known secret patterns from text. Returns the scrubbed string.
29
- * Safe to call on any text — if no patterns match, returns the original unchanged.
30
- */
31
- export function scrubSecrets(text: string): string {
32
- let result = text;
33
- for (const { pattern, label } of SECRET_PATTERNS) {
34
- // Reset lastIndex for global regexes
35
- pattern.lastIndex = 0;
36
- result = result.replace(pattern, label);
37
- }
38
- return result;
39
- }
package/src/setup.ts DELETED
@@ -1,384 +0,0 @@
1
- import { chmodSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
- import { homedir } from 'node:os';
3
- import { dirname, resolve } from 'node:path';
4
- import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
5
- import * as clack from '@clack/prompts';
6
- import {
7
- encryptSecretPayload,
8
- getDefaultSecureStorePath,
9
- readConfigFile,
10
- type LocalSecretPayload,
11
- type RuntimeConfigFile,
12
- writeConfigFile,
13
- } from './config.js';
14
- import { printBanner, printStep, printDone, printInfo, printError, printSuccess, pc } from './ui.js';
15
-
16
- interface BootstrapPayload {
17
- apiToken: string;
18
- walletPrivateKey?: string;
19
- llm?: {
20
- provider?: string;
21
- model?: string;
22
- apiKey?: string;
23
- };
24
- }
25
-
26
- function expandHomeDir(path: string): string {
27
- if (!path.startsWith('~/')) return path;
28
- return resolve(homedir(), path.slice(2));
29
- }
30
-
31
- function cancelled(): never {
32
- clack.cancel('Setup cancelled.');
33
- process.exit(0);
34
- }
35
-
36
- function isNonInteractive(): boolean {
37
- return process.env.EXAGENT_NONINTERACTIVE === '1' || process.env.EXAGENT_NONINTERACTIVE === 'true';
38
- }
39
-
40
- const LLM_KEY_PREFIXES: Record<string, string> = {
41
- openai: 'sk-',
42
- anthropic: 'sk-ant-',
43
- };
44
-
45
- function validateLlmKeyFormat(provider: string, key: string): string | undefined {
46
- if (!key.trim()) return 'API key is required.';
47
- const expectedPrefix = LLM_KEY_PREFIXES[provider];
48
- if (expectedPrefix && !key.startsWith(expectedPrefix)) {
49
- return `${provider} API keys typically start with "${expectedPrefix}". Double-check your key.`;
50
- }
51
- if (key.length < 10) return 'API key seems too short.';
52
- }
53
-
54
- // ---------------------------------------------------------------------------
55
- // Step 1: Bootstrap
56
- // ---------------------------------------------------------------------------
57
-
58
- async function consumeBootstrapPackage(config: RuntimeConfigFile): Promise<BootstrapPayload> {
59
- if (!config.secrets?.bootstrapToken) {
60
- return { apiToken: '' };
61
- }
62
-
63
- const apiHost = new URL(config.apiUrl).host;
64
- printInfo(`Connecting to ${pc.cyan(apiHost)}...`);
65
-
66
- const res = await fetch(`${config.apiUrl}/v1/agents/bootstrap/consume`, {
67
- method: 'POST',
68
- headers: { 'Content-Type': 'application/json' },
69
- body: JSON.stringify({
70
- agentId: config.agentId,
71
- token: config.secrets.bootstrapToken,
72
- }),
73
- });
74
-
75
- if (!res.ok) {
76
- const body = await res.text();
77
- throw new Error(`Failed to consume bootstrap package: ${body}`);
78
- }
79
-
80
- const data = await res.json() as { payload: BootstrapPayload };
81
- return data.payload;
82
- }
83
-
84
- // ---------------------------------------------------------------------------
85
- // Step 2: Wallet
86
- // ---------------------------------------------------------------------------
87
-
88
- async function setupWallet(config: RuntimeConfigFile): Promise<string> {
89
- if (config.wallet?.privateKey) {
90
- const account = privateKeyToAccount(config.wallet.privateKey as `0x${string}`);
91
- printDone(`Using existing wallet: ${pc.dim(account.address)}`);
92
- return config.wallet.privateKey;
93
- }
94
-
95
- // Non-interactive: env var wallet
96
- if (isNonInteractive()) {
97
- const mode = process.env.EXAGENT_WALLET_MODE || 'generate';
98
- if (mode === 'import') {
99
- const key = process.env.EXAGENT_WALLET_KEY;
100
- if (!key || !/^0x[a-fA-F0-9]{64}$/.test(key)) {
101
- throw new Error('EXAGENT_WALLET_KEY must be a valid 0x-prefixed 64-char hex private key in non-interactive mode');
102
- }
103
- const address = privateKeyToAccount(key as `0x${string}`).address;
104
- printDone(`Wallet imported: ${pc.dim(address)}`);
105
- return key;
106
- }
107
- const privateKey = generatePrivateKey();
108
- const address = privateKeyToAccount(privateKey).address;
109
- printDone(`Wallet created: ${pc.dim(address)}`);
110
- return privateKey;
111
- }
112
-
113
- const method = await clack.select({
114
- message: 'How would you like to set up your wallet?',
115
- options: [
116
- { value: 'generate', label: 'Generate new wallet locally', hint: 'recommended' },
117
- { value: 'import', label: 'Import existing private key' },
118
- ],
119
- });
120
- if (clack.isCancel(method)) cancelled();
121
-
122
- if (method === 'generate') {
123
- const privateKey = generatePrivateKey();
124
- const address = privateKeyToAccount(privateKey).address;
125
- printDone(`Wallet created: ${pc.dim(address)}`);
126
- return privateKey;
127
- }
128
-
129
- // Import flow
130
- const privateKey = await clack.password({
131
- message: 'Wallet private key (0x...):',
132
- validate: (val) => {
133
- if (!/^0x[a-fA-F0-9]{64}$/.test(val)) {
134
- return 'Invalid private key. Expected a 32-byte hex string prefixed with 0x.';
135
- }
136
- },
137
- });
138
- if (clack.isCancel(privateKey)) cancelled();
139
-
140
- const address = privateKeyToAccount(privateKey as `0x${string}`).address;
141
- printDone(`Wallet imported: ${pc.dim(address)}`);
142
- return privateKey;
143
- }
144
-
145
- // ---------------------------------------------------------------------------
146
- // Step 3: LLM
147
- // ---------------------------------------------------------------------------
148
-
149
- import { LLM_PROVIDERS, getProvider } from './llm-providers.js';
150
-
151
- async function setupLlm(
152
- config: RuntimeConfigFile,
153
- ): Promise<{ provider: string; model: string; apiKey: string }> {
154
- // LLM config is always entered locally — never pulled from bootstrap.
155
- // Config file may have provider/model as defaults from the deploy wizard.
156
-
157
- // Non-interactive mode
158
- if (isNonInteractive()) {
159
- const provider = process.env.EXAGENT_LLM_PROVIDER || config.llm?.provider;
160
- const model = process.env.EXAGENT_LLM_MODEL || config.llm?.model;
161
- const apiKey = process.env.EXAGENT_LLM_KEY;
162
- if (!provider) throw new Error('EXAGENT_LLM_PROVIDER required in non-interactive mode');
163
- if (!model) throw new Error('EXAGENT_LLM_MODEL required in non-interactive mode');
164
- if (!apiKey) throw new Error('EXAGENT_LLM_KEY required in non-interactive mode');
165
- printDone('LLM configured');
166
- return { provider, model, apiKey };
167
- }
168
-
169
- // Provider — use config as default selection if available
170
- const defaultProvider = config.llm?.provider;
171
- const providerOptions = LLM_PROVIDERS.map(p => ({ value: p.id, label: p.label }));
172
- const selected = await clack.select({
173
- message: 'LLM provider:',
174
- options: providerOptions,
175
- initialValue: defaultProvider || undefined,
176
- });
177
- if (clack.isCancel(selected)) cancelled();
178
- const provider = selected;
179
-
180
- // Model — show available models for the selected provider
181
- const defaultModel = config.llm?.model;
182
- const providerInfo = getProvider(provider);
183
- const modelOptions = providerInfo
184
- ? providerInfo.models.map(m => ({ value: m.id, label: m.label }))
185
- : [{ value: defaultModel || 'gpt-4o', label: defaultModel || 'gpt-4o' }];
186
- const selectedModel = await clack.select({
187
- message: 'LLM model:',
188
- options: modelOptions,
189
- initialValue: defaultModel || undefined,
190
- });
191
- if (clack.isCancel(selectedModel)) cancelled();
192
- const model = selectedModel;
193
-
194
- // API Key — always prompt, never from bootstrap
195
- const apiKey = await clack.password({
196
- message: 'LLM API key:',
197
- validate: (val) => validateLlmKeyFormat(provider, val),
198
- });
199
- if (clack.isCancel(apiKey)) cancelled();
200
-
201
- printDone('LLM configured');
202
- return { provider, model, apiKey };
203
- }
204
-
205
- // ---------------------------------------------------------------------------
206
- // Step 4: Encryption
207
- // ---------------------------------------------------------------------------
208
-
209
- async function setupEncryption(): Promise<string> {
210
- // Non-interactive mode
211
- if (isNonInteractive()) {
212
- const password = process.env.EXAGENT_PASSWORD;
213
- if (!password || password.length < 12) {
214
- throw new Error('EXAGENT_PASSWORD must be at least 12 characters in non-interactive mode');
215
- }
216
- return password;
217
- }
218
-
219
- printInfo(`Secrets encrypted with ${pc.cyan('AES-256-GCM')} (${pc.cyan('scrypt')} KDF)`);
220
- printInfo('The password never leaves this machine.');
221
- console.log();
222
-
223
- const password = await clack.password({
224
- message: 'Choose a device password (12+ characters):',
225
- validate: (val) => {
226
- if (val.length < 12) return 'Password must be at least 12 characters.';
227
- },
228
- });
229
- if (clack.isCancel(password)) cancelled();
230
-
231
- const confirm = await clack.password({
232
- message: 'Confirm password:',
233
- validate: (val) => {
234
- if (val !== password) return 'Passwords do not match.';
235
- },
236
- });
237
- if (clack.isCancel(confirm)) cancelled();
238
-
239
- return password;
240
- }
241
-
242
- // ---------------------------------------------------------------------------
243
- // Secure store
244
- // ---------------------------------------------------------------------------
245
-
246
- function writeSecureStore(path: string, secrets: LocalSecretPayload, password: string): string {
247
- const secureStorePath = expandHomeDir(path);
248
- const encrypted = encryptSecretPayload(secrets, password);
249
- const dir = dirname(secureStorePath);
250
- if (!existsSync(dir)) {
251
- mkdirSync(dir, { recursive: true, mode: 0o700 });
252
- }
253
- writeFileSync(secureStorePath, JSON.stringify(encrypted, null, 2), { mode: 0o600 });
254
- try {
255
- chmodSync(secureStorePath, 0o600);
256
- } catch {
257
- // Best effort — some platforms ignore chmod semantics.
258
- }
259
- return secureStorePath;
260
- }
261
-
262
- // ---------------------------------------------------------------------------
263
- // Public: password prompt (used by run/status commands)
264
- // ---------------------------------------------------------------------------
265
-
266
- export async function promptSecretPassword(question: string = 'Device password:'): Promise<string> {
267
- if (isNonInteractive()) {
268
- const password = process.env.EXAGENT_PASSWORD || process.env.EXAGENT_SECRET_PASSWORD;
269
- if (!password) throw new Error('EXAGENT_PASSWORD required in non-interactive mode');
270
- return password;
271
- }
272
- const password = await clack.password({ message: question });
273
- if (clack.isCancel(password)) cancelled();
274
- return password;
275
- }
276
-
277
- // ---------------------------------------------------------------------------
278
- // Main setup orchestrator
279
- // ---------------------------------------------------------------------------
280
-
281
- export async function ensureLocalSetup(configPath: string): Promise<void> {
282
- const config = readConfigFile(configPath);
283
- const existingSecureStorePath = config.secrets?.secureStorePath ? expandHomeDir(config.secrets.secureStorePath) : null;
284
- if (
285
- existingSecureStorePath &&
286
- !config.secrets?.bootstrapToken &&
287
- existsSync(existingSecureStorePath) &&
288
- !config.apiToken &&
289
- !config.wallet?.privateKey &&
290
- !config.llm.apiKey
291
- ) {
292
- printBanner();
293
- printSuccess('Already set up', [
294
- `${pc.cyan('npx exagent run')} Start the agent`,
295
- `${pc.cyan('npx exagent config')} Change LLM API key or model`,
296
- `${pc.cyan('npx exagent status')} Check agent connection`,
297
- '',
298
- `${pc.dim('Dashboard:')} ${pc.cyan('https://exagent.io')}`,
299
- ]);
300
- return;
301
- }
302
-
303
- printBanner();
304
-
305
- clack.intro(pc.bold('Agent Setup'));
306
-
307
- // Step 1: Bootstrap
308
- printStep(1, 4, 'Bootstrap package');
309
- const bootstrapPayload = await consumeBootstrapPackage(config);
310
- if (config.secrets?.bootstrapToken) {
311
- printDone('Bootstrap package consumed');
312
- } else {
313
- printInfo('No bootstrap token — manual configuration');
314
- }
315
-
316
- // Step 2: Wallet
317
- printStep(2, 4, 'Wallet setup');
318
- const walletPrivateKey = await setupWallet(config);
319
-
320
- // Step 3: LLM
321
- printStep(3, 4, 'LLM configuration');
322
- const llm = await setupLlm(config);
323
-
324
- // Step 4: Encryption
325
- printStep(4, 4, 'Device encryption');
326
- const password = await setupEncryption();
327
-
328
- // Build secrets and write
329
- const secrets: LocalSecretPayload = {
330
- apiToken: bootstrapPayload.apiToken || config.apiToken || '',
331
- walletPrivateKey,
332
- llmApiKey: llm.apiKey,
333
- };
334
-
335
- if (!secrets.apiToken) {
336
- if (isNonInteractive()) {
337
- const token = process.env.EXAGENT_API_TOKEN;
338
- if (!token) throw new Error('EXAGENT_API_TOKEN required in non-interactive mode (no relay token from bootstrap)');
339
- secrets.apiToken = token;
340
- } else {
341
- const token = await clack.password({
342
- message: 'Agent relay token:',
343
- validate: (val) => {
344
- if (!val.trim()) return 'Relay token is required.';
345
- },
346
- });
347
- if (clack.isCancel(token)) cancelled();
348
- secrets.apiToken = token;
349
- }
350
- }
351
-
352
- const nextConfig = structuredClone(config);
353
- nextConfig.llm = {
354
- ...nextConfig.llm,
355
- provider: llm.provider as RuntimeConfigFile['llm']['provider'],
356
- model: llm.model,
357
- };
358
-
359
- // Strip plaintext secrets from config file
360
- delete nextConfig.apiToken;
361
- delete nextConfig.wallet;
362
- delete nextConfig.llm.apiKey;
363
-
364
- const secureStorePath = writeSecureStore(
365
- nextConfig.secrets?.secureStorePath || getDefaultSecureStorePath(nextConfig.agentId),
366
- secrets,
367
- password,
368
- );
369
-
370
- nextConfig.secrets = { secureStorePath };
371
- writeConfigFile(configPath, nextConfig);
372
-
373
- printDone(`Encrypted store: ${pc.dim(secureStorePath)}`);
374
-
375
- clack.outro(pc.green('Setup complete'));
376
-
377
- printSuccess('Ready', [
378
- `${pc.cyan('npx exagent run')} Start the agent`,
379
- `${pc.cyan('npx exagent config')} Change LLM API key or model`,
380
- `${pc.cyan('npx exagent status')} Check agent connection`,
381
- '',
382
- `${pc.dim('Dashboard:')} ${pc.cyan('https://exagent.io')}`,
383
- ]);
384
- }
package/src/signal.ts DELETED
@@ -1,212 +0,0 @@
1
- import type { TradeSignal } from '@exagent/sdk';
2
- import type { RelayClient } from './relay.js';
3
- import type { FileStore } from './store.js';
4
- import { getLogger } from './logger.js';
5
-
6
- const SIGNAL_QUEUE_KEY = 'pending_trade_signals';
7
- const MAX_QUEUE_SIZE = 1000;
8
-
9
- interface QueuedSignal {
10
- signal: TradeSignal;
11
- reportType: 'trade' | 'perp_fill' | 'prediction_fill' | 'spot_fill' | 'bridge_fill';
12
- queuedAt: number;
13
- }
14
-
15
- export class SignalReporter {
16
- private relay: RelayClient;
17
- private store: FileStore | null;
18
- private pendingSignals: QueuedSignal[] = [];
19
-
20
- constructor(relay: RelayClient, store?: FileStore) {
21
- this.relay = relay;
22
- this.store = store ?? null;
23
-
24
- // Load persisted queue from store (survives process crashes)
25
- if (this.store) {
26
- const persisted = this.store.get<QueuedSignal[]>(SIGNAL_QUEUE_KEY);
27
- if (persisted && Array.isArray(persisted)) {
28
- this.pendingSignals = persisted;
29
- if (this.pendingSignals.length > 0) {
30
- getLogger().info('signal', 'Loaded queued signals from disk', { count: this.pendingSignals.length });
31
- }
32
- }
33
- }
34
- }
35
-
36
- /** Flush all queued signals to the relay. Called on reconnect. */
37
- flushQueue(): void {
38
- if (this.pendingSignals.length === 0) return;
39
- if (!this.relay.isConnected) return;
40
-
41
- getLogger().info('signal', 'Flushing queued signals', { count: this.pendingSignals.length });
42
- const signals = [...this.pendingSignals];
43
- this.pendingSignals = [];
44
- this.persistQueue();
45
-
46
- for (const queued of signals) {
47
- this.relay.sendTradeSignal(queued.signal);
48
-
49
- // Also send the human-readable message
50
- const sig = queued.signal;
51
- switch (queued.reportType) {
52
- case 'trade':
53
- this.relay.sendMessage(
54
- 'trade_executed', 'success',
55
- `${sig.side.toUpperCase()} ${sig.symbol}`,
56
- `${sig.side} ${sig.size} ${sig.symbol} @ $${sig.price.toFixed(2)} on ${sig.venue} (queued)`,
57
- { signal: sig },
58
- );
59
- break;
60
- case 'perp_fill':
61
- this.relay.sendMessage(
62
- 'perp_fill', 'success',
63
- `Perp ${sig.side.toUpperCase()} ${sig.symbol}`,
64
- `${sig.side} ${sig.size} ${sig.symbol} @ $${sig.price.toFixed(2)} (${sig.leverage ?? 1}x) (queued)`,
65
- { signal: sig },
66
- );
67
- break;
68
- case 'prediction_fill':
69
- this.relay.sendMessage(
70
- 'prediction_fill', 'success',
71
- `Prediction ${sig.side.toUpperCase()}`,
72
- `${sig.side} $${sig.size.toFixed(2)} on ${sig.symbol} @ ${sig.price.toFixed(4)} (queued)`,
73
- { signal: sig },
74
- );
75
- break;
76
- case 'spot_fill':
77
- this.relay.sendMessage(
78
- 'spot_fill', 'success',
79
- `Spot ${sig.side.toUpperCase()} ${sig.symbol}`,
80
- `${sig.side} ${sig.size} ${sig.symbol} @ $${sig.price.toFixed(4)} on ${sig.venue} (${sig.chain ?? 'unknown'}) (queued)`,
81
- { signal: sig },
82
- );
83
- break;
84
- case 'bridge_fill':
85
- this.relay.sendMessage(
86
- 'bridge_fill', 'success',
87
- `Bridge ${sig.symbol}`,
88
- `Bridged ${sig.size} ${sig.symbol} to ${sig.chain ?? 'unknown'} via ${sig.venue} (queued)`,
89
- { signal: sig },
90
- );
91
- break;
92
- }
93
- }
94
-
95
- getLogger().info('signal', 'Queue flushed successfully');
96
- }
97
-
98
- get queueSize(): number {
99
- return this.pendingSignals.length;
100
- }
101
-
102
- private enqueue(signal: TradeSignal, reportType: QueuedSignal['reportType']): void {
103
- this.pendingSignals.push({ signal, reportType, queuedAt: Date.now() });
104
-
105
- // Evict oldest if over cap
106
- while (this.pendingSignals.length > MAX_QUEUE_SIZE) {
107
- this.pendingSignals.shift();
108
- }
109
-
110
- this.persistQueue();
111
- }
112
-
113
- private persistQueue(): void {
114
- if (!this.store) return;
115
- if (this.pendingSignals.length === 0) {
116
- this.store.delete(SIGNAL_QUEUE_KEY);
117
- } else {
118
- this.store.set(SIGNAL_QUEUE_KEY, this.pendingSignals);
119
- }
120
- }
121
-
122
- reportTrade(signal: TradeSignal): void {
123
- if (!this.relay.isConnected) {
124
- getLogger().warn('signal', 'Not connected to relay — trade signal queued locally', { symbol: signal.symbol });
125
- this.enqueue(signal, 'trade');
126
- return;
127
- }
128
-
129
- this.relay.sendTradeSignal(signal);
130
- this.relay.sendMessage(
131
- 'trade_executed',
132
- 'success',
133
- `${signal.side.toUpperCase()} ${signal.symbol}`,
134
- `${signal.side} ${signal.size} ${signal.symbol} @ $${signal.price.toFixed(2)} on ${signal.venue}`,
135
- { signal },
136
- );
137
- }
138
-
139
- reportPerpFill(signal: TradeSignal): void {
140
- if (!this.relay.isConnected) {
141
- this.enqueue(signal, 'perp_fill');
142
- return;
143
- }
144
-
145
- this.relay.sendTradeSignal(signal);
146
- this.relay.sendMessage(
147
- 'perp_fill',
148
- 'success',
149
- `Perp ${signal.side.toUpperCase()} ${signal.symbol}`,
150
- `${signal.side} ${signal.size} ${signal.symbol} @ $${signal.price.toFixed(2)} (${signal.leverage ?? 1}x)`,
151
- { signal },
152
- );
153
- }
154
-
155
- reportPredictionFill(signal: TradeSignal): void {
156
- if (!this.relay.isConnected) {
157
- this.enqueue(signal, 'prediction_fill');
158
- return;
159
- }
160
-
161
- this.relay.sendTradeSignal(signal);
162
- this.relay.sendMessage(
163
- 'prediction_fill',
164
- 'success',
165
- `Prediction ${signal.side.toUpperCase()}`,
166
- `${signal.side} $${signal.size.toFixed(2)} on ${signal.symbol} @ ${signal.price.toFixed(4)}`,
167
- { signal },
168
- );
169
- }
170
-
171
- reportSpotFill(signal: TradeSignal): void {
172
- if (!this.relay.isConnected) {
173
- this.enqueue(signal, 'spot_fill');
174
- return;
175
- }
176
-
177
- this.relay.sendTradeSignal(signal);
178
- this.relay.sendMessage(
179
- 'spot_fill',
180
- 'success',
181
- `Spot ${signal.side.toUpperCase()} ${signal.symbol}`,
182
- `${signal.side} ${signal.size} ${signal.symbol} @ $${signal.price.toFixed(4)} on ${signal.venue} (${signal.chain ?? 'unknown'})`,
183
- { signal },
184
- );
185
- }
186
-
187
- reportBridgeFill(signal: TradeSignal): void {
188
- if (!this.relay.isConnected) {
189
- this.enqueue(signal, 'bridge_fill');
190
- return;
191
- }
192
-
193
- this.relay.sendTradeSignal(signal);
194
- this.relay.sendMessage(
195
- 'bridge_fill',
196
- 'success',
197
- `Bridge ${signal.symbol}`,
198
- `Bridged ${signal.size} ${signal.symbol} to ${signal.chain ?? 'unknown'} via ${signal.venue}`,
199
- { signal },
200
- );
201
- }
202
-
203
- reportError(title: string, body: string, data?: Record<string, unknown>): void {
204
- if (!this.relay.isConnected) return;
205
- this.relay.sendMessage('error', 'error', title, body, data);
206
- }
207
-
208
- reportInfo(title: string, body: string, data?: Record<string, unknown>): void {
209
- if (!this.relay.isConnected) return;
210
- this.relay.sendMessage('info', 'info', title, body, data);
211
- }
212
- }