@exagent/agent 0.3.6 → 0.3.8

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 (69) 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-GYYW4EKM.js +6756 -0
  5. package/dist/chunk-IVA2SCSN.js +6756 -0
  6. package/dist/chunk-JHXCSGPC.js +6352 -0
  7. package/dist/chunk-V6O4UXVN.js +6345 -0
  8. package/dist/chunk-WTECTX2Z.js +6345 -0
  9. package/dist/cli.js +2 -2
  10. package/dist/index.d.ts +24 -2
  11. package/dist/index.js +1 -1
  12. package/package.json +12 -9
  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 -250
  19. package/src/config.ts +0 -502
  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 -94
  31. package/src/llm/together.ts +0 -48
  32. package/src/llm-providers.ts +0 -8
  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 -288
  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 -392
  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 -265
  63. package/src/strategy/templates.ts +0 -74
  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/strategy-loader.test.ts +0 -150
  69. 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,392 +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, getDefaultModel, getProvider, providerRequiresApiKey } 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 (providerRequiresApiKey(provider) && !apiKey) {
165
- throw new Error('EXAGENT_LLM_KEY required in non-interactive mode');
166
- }
167
- printDone('LLM configured');
168
- return { provider, model, apiKey };
169
- }
170
-
171
- // Provider — use config as default selection if available
172
- const defaultProvider = config.llm?.provider;
173
- const providerOptions = LLM_PROVIDERS.map(p => ({ value: p.id, label: p.label }));
174
- const selected = await clack.select({
175
- message: 'LLM provider:',
176
- options: providerOptions,
177
- initialValue: defaultProvider || undefined,
178
- });
179
- if (clack.isCancel(selected)) cancelled();
180
- const provider = selected;
181
-
182
- // Model — show available models for the selected provider
183
- const defaultModel = config.llm?.model;
184
- const providerInfo = getProvider(provider);
185
- const modelOptions = providerInfo
186
- ? providerInfo.models.map(m => ({ value: m.id, label: m.label }))
187
- : [{ value: defaultModel || getDefaultModel('openai'), label: defaultModel || getDefaultModel('openai') }];
188
- const selectedModel = await clack.select({
189
- message: 'LLM model:',
190
- options: modelOptions,
191
- initialValue: defaultModel || undefined,
192
- });
193
- if (clack.isCancel(selectedModel)) cancelled();
194
- const model = selectedModel;
195
-
196
- let apiKey: string | undefined;
197
- if (providerRequiresApiKey(provider)) {
198
- // API Key — always prompt, never from bootstrap
199
- const enteredApiKey = await clack.password({
200
- message: 'LLM API key:',
201
- validate: (val) => validateLlmKeyFormat(provider, val),
202
- });
203
- if (clack.isCancel(enteredApiKey)) cancelled();
204
- apiKey = enteredApiKey;
205
- } else {
206
- printInfo('Ollama uses your local server; no API key needed.');
207
- }
208
-
209
- printDone('LLM configured');
210
- return { provider, model, apiKey };
211
- }
212
-
213
- // ---------------------------------------------------------------------------
214
- // Step 4: Encryption
215
- // ---------------------------------------------------------------------------
216
-
217
- async function setupEncryption(): Promise<string> {
218
- // Non-interactive mode
219
- if (isNonInteractive()) {
220
- const password = process.env.EXAGENT_PASSWORD;
221
- if (!password || password.length < 12) {
222
- throw new Error('EXAGENT_PASSWORD must be at least 12 characters in non-interactive mode');
223
- }
224
- return password;
225
- }
226
-
227
- printInfo(`Secrets encrypted with ${pc.cyan('AES-256-GCM')} (${pc.cyan('scrypt')} KDF)`);
228
- printInfo('The password never leaves this machine.');
229
- console.log();
230
-
231
- const password = await clack.password({
232
- message: 'Choose a device password (12+ characters):',
233
- validate: (val) => {
234
- if (val.length < 12) return 'Password must be at least 12 characters.';
235
- },
236
- });
237
- if (clack.isCancel(password)) cancelled();
238
-
239
- const confirm = await clack.password({
240
- message: 'Confirm password:',
241
- validate: (val) => {
242
- if (val !== password) return 'Passwords do not match.';
243
- },
244
- });
245
- if (clack.isCancel(confirm)) cancelled();
246
-
247
- return password;
248
- }
249
-
250
- // ---------------------------------------------------------------------------
251
- // Secure store
252
- // ---------------------------------------------------------------------------
253
-
254
- function writeSecureStore(path: string, secrets: LocalSecretPayload, password: string): string {
255
- const secureStorePath = expandHomeDir(path);
256
- const encrypted = encryptSecretPayload(secrets, password);
257
- const dir = dirname(secureStorePath);
258
- if (!existsSync(dir)) {
259
- mkdirSync(dir, { recursive: true, mode: 0o700 });
260
- }
261
- writeFileSync(secureStorePath, JSON.stringify(encrypted, null, 2), { mode: 0o600 });
262
- try {
263
- chmodSync(secureStorePath, 0o600);
264
- } catch {
265
- // Best effort — some platforms ignore chmod semantics.
266
- }
267
- return secureStorePath;
268
- }
269
-
270
- // ---------------------------------------------------------------------------
271
- // Public: password prompt (used by run/status commands)
272
- // ---------------------------------------------------------------------------
273
-
274
- export async function promptSecretPassword(question: string = 'Device password:'): Promise<string> {
275
- if (isNonInteractive()) {
276
- const password = process.env.EXAGENT_PASSWORD || process.env.EXAGENT_SECRET_PASSWORD;
277
- if (!password) throw new Error('EXAGENT_PASSWORD required in non-interactive mode');
278
- return password;
279
- }
280
- const password = await clack.password({ message: question });
281
- if (clack.isCancel(password)) cancelled();
282
- return password;
283
- }
284
-
285
- // ---------------------------------------------------------------------------
286
- // Main setup orchestrator
287
- // ---------------------------------------------------------------------------
288
-
289
- export async function ensureLocalSetup(configPath: string): Promise<void> {
290
- const config = readConfigFile(configPath);
291
- const existingSecureStorePath = config.secrets?.secureStorePath ? expandHomeDir(config.secrets.secureStorePath) : null;
292
- if (
293
- existingSecureStorePath &&
294
- !config.secrets?.bootstrapToken &&
295
- existsSync(existingSecureStorePath) &&
296
- !config.apiToken &&
297
- !config.wallet?.privateKey &&
298
- !config.llm.apiKey
299
- ) {
300
- printBanner();
301
- printSuccess('Already set up', [
302
- `${pc.cyan('npx exagent run')} Start the agent`,
303
- `${pc.cyan('npx exagent config')} Change LLM API key or model`,
304
- `${pc.cyan('npx exagent status')} Check agent connection`,
305
- '',
306
- `${pc.dim('Dashboard:')} ${pc.cyan('https://exagent.io')}`,
307
- ]);
308
- return;
309
- }
310
-
311
- printBanner();
312
-
313
- clack.intro(pc.bold('Agent Setup'));
314
-
315
- // Step 1: Bootstrap
316
- printStep(1, 4, 'Bootstrap package');
317
- const bootstrapPayload = await consumeBootstrapPackage(config);
318
- if (config.secrets?.bootstrapToken) {
319
- printDone('Bootstrap package consumed');
320
- } else {
321
- printInfo('No bootstrap token — manual configuration');
322
- }
323
-
324
- // Step 2: Wallet
325
- printStep(2, 4, 'Wallet setup');
326
- const walletPrivateKey = await setupWallet(config);
327
-
328
- // Step 3: LLM
329
- printStep(3, 4, 'LLM configuration');
330
- const llm = await setupLlm(config);
331
-
332
- // Step 4: Encryption
333
- printStep(4, 4, 'Device encryption');
334
- const password = await setupEncryption();
335
-
336
- // Build secrets and write
337
- const secrets: LocalSecretPayload = {
338
- apiToken: bootstrapPayload.apiToken || config.apiToken || '',
339
- walletPrivateKey,
340
- llmApiKey: llm.apiKey,
341
- };
342
-
343
- if (!secrets.apiToken) {
344
- if (isNonInteractive()) {
345
- const token = process.env.EXAGENT_API_TOKEN;
346
- if (!token) throw new Error('EXAGENT_API_TOKEN required in non-interactive mode (no relay token from bootstrap)');
347
- secrets.apiToken = token;
348
- } else {
349
- const token = await clack.password({
350
- message: 'Agent relay token:',
351
- validate: (val) => {
352
- if (!val.trim()) return 'Relay token is required.';
353
- },
354
- });
355
- if (clack.isCancel(token)) cancelled();
356
- secrets.apiToken = token;
357
- }
358
- }
359
-
360
- const nextConfig = structuredClone(config);
361
- nextConfig.llm = {
362
- ...nextConfig.llm,
363
- provider: llm.provider as RuntimeConfigFile['llm']['provider'],
364
- model: llm.model,
365
- };
366
-
367
- // Strip plaintext secrets from config file
368
- delete nextConfig.apiToken;
369
- delete nextConfig.wallet;
370
- delete nextConfig.llm.apiKey;
371
-
372
- const secureStorePath = writeSecureStore(
373
- nextConfig.secrets?.secureStorePath || getDefaultSecureStorePath(nextConfig.agentId),
374
- secrets,
375
- password,
376
- );
377
-
378
- nextConfig.secrets = { secureStorePath };
379
- writeConfigFile(configPath, nextConfig);
380
-
381
- printDone(`Encrypted store: ${pc.dim(secureStorePath)}`);
382
-
383
- clack.outro(pc.green('Setup complete'));
384
-
385
- printSuccess('Ready', [
386
- `${pc.cyan('npx exagent run')} Start the agent`,
387
- `${pc.cyan('npx exagent config')} Change LLM API key or model`,
388
- `${pc.cyan('npx exagent status')} Check agent connection`,
389
- '',
390
- `${pc.dim('Dashboard:')} ${pc.cyan('https://exagent.io')}`,
391
- ]);
392
- }
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
- }