@exagent/agent 0.3.4 → 0.3.6
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-WTECTX2Z.js → chunk-ZRAOPQQW.js} +208 -147
- package/dist/cli.js +57 -37
- package/dist/index.js +1 -1
- package/package.json +18 -18
- package/src/cli.ts +32 -23
- package/src/config.ts +6 -3
- package/src/llm/anthropic.ts +3 -3
- package/src/llm/deepseek.ts +3 -3
- package/src/llm/google.ts +3 -3
- package/src/llm/groq.ts +3 -3
- package/src/llm/mistral.ts +3 -3
- package/src/llm/ollama.ts +3 -3
- package/src/llm/openai.ts +46 -3
- package/src/llm/together.ts +3 -3
- package/src/llm-providers.ts +8 -0
- package/src/prediction/client.ts +11 -4
- package/src/runtime.ts +3 -3
- package/src/setup.ts +29 -20
- package/src/strategy/loader.ts +136 -62
- package/src/strategy/templates.ts +0 -51
- package/test/strategy-loader.test.ts +150 -0
- package/.turbo/turbo-build.log +0 -17
- 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/src/prediction/client.ts
CHANGED
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { ClobClient, Side } from '@polymarket/clob-client';
|
|
12
|
-
import {
|
|
12
|
+
import { createWalletClient, http, type WalletClient } from 'viem';
|
|
13
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
14
|
+
import { polygon } from 'viem/chains';
|
|
13
15
|
import type {
|
|
14
16
|
PredictionConfig,
|
|
15
17
|
PredictionMarket,
|
|
@@ -32,7 +34,7 @@ export class PolymarketClient {
|
|
|
32
34
|
private readonly config: PredictionConfig;
|
|
33
35
|
private clobClient: ClobClient | null = null;
|
|
34
36
|
private apiCreds: ApiKeyCreds | null = null;
|
|
35
|
-
private readonly signer:
|
|
37
|
+
private readonly signer: WalletClient;
|
|
36
38
|
private readonly walletAddress: string;
|
|
37
39
|
|
|
38
40
|
private marketCache: Map<string, { market: PredictionMarket; cachedAt: number }> = new Map();
|
|
@@ -40,8 +42,13 @@ export class PolymarketClient {
|
|
|
40
42
|
|
|
41
43
|
constructor(privateKey: string, config?: Partial<PredictionConfig>) {
|
|
42
44
|
this.config = { ...DEFAULT_PREDICTION_CONFIG, ...config } as PredictionConfig;
|
|
43
|
-
|
|
44
|
-
this.
|
|
45
|
+
const account = privateKeyToAccount(privateKey as `0x${string}`);
|
|
46
|
+
this.signer = createWalletClient({
|
|
47
|
+
account,
|
|
48
|
+
chain: polygon,
|
|
49
|
+
transport: http(),
|
|
50
|
+
});
|
|
51
|
+
this.walletAddress = account.address;
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
// ── INITIALIZATION ─────────────────────────────────────────
|
package/src/runtime.ts
CHANGED
|
@@ -52,7 +52,7 @@ import type { BridgeConfig } from './bridge/types.js';
|
|
|
52
52
|
|
|
53
53
|
import { createRequire } from 'module';
|
|
54
54
|
const _require = createRequire(import.meta.url);
|
|
55
|
-
let SDK_VERSION = '0.3.
|
|
55
|
+
let SDK_VERSION = '0.3.5';
|
|
56
56
|
try { SDK_VERSION = _require('../package.json').version; } catch {}
|
|
57
57
|
|
|
58
58
|
/** Number of consecutive cycle failures before switching to idle */
|
|
@@ -427,7 +427,7 @@ export class AgentRuntime {
|
|
|
427
427
|
: 'Strategy description: follow the owner-configured strategy exactly and do not improvise outside it.',
|
|
428
428
|
`Allowed venues: ${venues}.`,
|
|
429
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
|
|
430
|
+
'Return ONLY a JSON array of trade intents.',
|
|
431
431
|
].filter((line): line is string => Boolean(line)).join('\n');
|
|
432
432
|
}
|
|
433
433
|
|
|
@@ -446,7 +446,7 @@ export class AgentRuntime {
|
|
|
446
446
|
}
|
|
447
447
|
|
|
448
448
|
if (typeof rawStrategy.code === 'string' && rawStrategy.code.trim()) {
|
|
449
|
-
|
|
449
|
+
throw new Error('Raw JavaScript strategy code is disabled. Use a prompt-backed strategy or template instead.');
|
|
450
450
|
}
|
|
451
451
|
|
|
452
452
|
if (typeof rawStrategy.systemPrompt === 'string' && rawStrategy.systemPrompt.trim()) {
|
package/src/setup.ts
CHANGED
|
@@ -146,11 +146,11 @@ async function setupWallet(config: RuntimeConfigFile): Promise<string> {
|
|
|
146
146
|
// Step 3: LLM
|
|
147
147
|
// ---------------------------------------------------------------------------
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
import { LLM_PROVIDERS, getDefaultModel, getProvider, providerRequiresApiKey } from './llm-providers.js';
|
|
150
150
|
|
|
151
151
|
async function setupLlm(
|
|
152
152
|
config: RuntimeConfigFile,
|
|
153
|
-
): Promise<{ provider: string; model: string; apiKey
|
|
153
|
+
): Promise<{ provider: string; model: string; apiKey?: string }> {
|
|
154
154
|
// LLM config is always entered locally — never pulled from bootstrap.
|
|
155
155
|
// Config file may have provider/model as defaults from the deploy wizard.
|
|
156
156
|
|
|
@@ -161,14 +161,16 @@ async function setupLlm(
|
|
|
161
161
|
const apiKey = process.env.EXAGENT_LLM_KEY;
|
|
162
162
|
if (!provider) throw new Error('EXAGENT_LLM_PROVIDER required in non-interactive mode');
|
|
163
163
|
if (!model) throw new Error('EXAGENT_LLM_MODEL required in non-interactive mode');
|
|
164
|
-
if (!apiKey)
|
|
164
|
+
if (providerRequiresApiKey(provider) && !apiKey) {
|
|
165
|
+
throw new Error('EXAGENT_LLM_KEY required in non-interactive mode');
|
|
166
|
+
}
|
|
165
167
|
printDone('LLM configured');
|
|
166
168
|
return { provider, model, apiKey };
|
|
167
169
|
}
|
|
168
170
|
|
|
169
171
|
// Provider — use config as default selection if available
|
|
170
172
|
const defaultProvider = config.llm?.provider;
|
|
171
|
-
const providerOptions = LLM_PROVIDERS.map(p => ({ value: p, label: p }));
|
|
173
|
+
const providerOptions = LLM_PROVIDERS.map(p => ({ value: p.id, label: p.label }));
|
|
172
174
|
const selected = await clack.select({
|
|
173
175
|
message: 'LLM provider:',
|
|
174
176
|
options: providerOptions,
|
|
@@ -177,25 +179,32 @@ async function setupLlm(
|
|
|
177
179
|
if (clack.isCancel(selected)) cancelled();
|
|
178
180
|
const provider = selected;
|
|
179
181
|
|
|
180
|
-
// Model —
|
|
182
|
+
// Model — show available models for the selected provider
|
|
181
183
|
const defaultModel = config.llm?.model;
|
|
182
|
-
const
|
|
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({
|
|
183
189
|
message: 'LLM model:',
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
validate: (val) => {
|
|
187
|
-
if (!val.trim()) return 'Model name is required.';
|
|
188
|
-
},
|
|
190
|
+
options: modelOptions,
|
|
191
|
+
initialValue: defaultModel || undefined,
|
|
189
192
|
});
|
|
190
|
-
if (clack.isCancel(
|
|
191
|
-
const model =
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
+
}
|
|
199
208
|
|
|
200
209
|
printDone('LLM configured');
|
|
201
210
|
return { provider, model, apiKey };
|
package/src/strategy/loader.ts
CHANGED
|
@@ -1,27 +1,36 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { resolve } from 'node:path';
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { extname, resolve } from 'node:path';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import type { StrategyFunction, StrategyContext, TradeSignal } from '@exagent/sdk';
|
|
5
5
|
import { getTemplate } from './templates.js';
|
|
6
6
|
import { scrubSecrets } from '../scrub-secrets.js';
|
|
7
7
|
|
|
8
|
+
const MAX_PROMPT_SIGNALS = 10;
|
|
9
|
+
const SUPPORTED_FILE_EXPORTS = new Set(['name', 'description', 'category', 'venues', 'systemPrompt', 'template']);
|
|
10
|
+
|
|
8
11
|
const promptSignalSchema = z.object({
|
|
9
|
-
symbol: z.string().min(1),
|
|
12
|
+
symbol: z.string().min(1).max(80),
|
|
10
13
|
side: z.enum(['buy', 'sell', 'long', 'short']),
|
|
11
14
|
confidence: z.number().min(0).max(1).optional(),
|
|
12
|
-
reasoning: z.string().optional(),
|
|
13
|
-
venue: z.string().optional(),
|
|
14
|
-
chain: z.string().optional(),
|
|
15
|
+
reasoning: z.string().max(1000).optional(),
|
|
16
|
+
venue: z.string().min(1).max(80).optional(),
|
|
17
|
+
chain: z.string().min(1).max(80).optional(),
|
|
15
18
|
size: z.number().positive().optional(),
|
|
16
19
|
price: z.number().positive().optional(),
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
});
|
|
20
|
+
leverage: z.number().positive().max(100).optional(),
|
|
21
|
+
orderType: z.enum(['market', 'limit', 'yes', 'no']).optional(),
|
|
22
|
+
}).strict();
|
|
23
|
+
|
|
24
|
+
const promptSignalArraySchema = z.array(promptSignalSchema).max(MAX_PROMPT_SIGNALS);
|
|
23
25
|
|
|
24
|
-
const
|
|
26
|
+
const strategyMetadataSchema = z.object({
|
|
27
|
+
name: z.string().min(1).max(120).optional(),
|
|
28
|
+
description: z.string().max(1000).optional(),
|
|
29
|
+
category: z.string().max(80).optional(),
|
|
30
|
+
venues: z.array(z.string().min(1).max(80)).max(20).optional(),
|
|
31
|
+
systemPrompt: z.string().min(1).max(12000).optional(),
|
|
32
|
+
template: z.string().min(1).max(80).optional(),
|
|
33
|
+
}).strict();
|
|
25
34
|
|
|
26
35
|
export async function loadStrategy(config: {
|
|
27
36
|
file?: string;
|
|
@@ -33,12 +42,12 @@ export async function loadStrategy(config: {
|
|
|
33
42
|
venues?: string[];
|
|
34
43
|
};
|
|
35
44
|
}): Promise<StrategyFunction> {
|
|
36
|
-
if (config.
|
|
37
|
-
|
|
45
|
+
if (config.code?.trim()) {
|
|
46
|
+
throw new Error('Raw JavaScript strategy code is disabled. Use a template or prompt-backed strategy instead.');
|
|
38
47
|
}
|
|
39
48
|
|
|
40
|
-
if (config.
|
|
41
|
-
return
|
|
49
|
+
if (config.file) {
|
|
50
|
+
return loadFromFile(config.file);
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
if (config.prompt) {
|
|
@@ -50,7 +59,14 @@ export async function loadStrategy(config: {
|
|
|
50
59
|
if (!template) {
|
|
51
60
|
throw new Error(`Unknown strategy template: ${config.template}. Available: momentum, value, arbitrage, hold`);
|
|
52
61
|
}
|
|
53
|
-
|
|
62
|
+
if (!template.systemPrompt.trim()) {
|
|
63
|
+
return holdStrategy;
|
|
64
|
+
}
|
|
65
|
+
return loadFromPrompt({
|
|
66
|
+
name: template.name,
|
|
67
|
+
systemPrompt: template.systemPrompt,
|
|
68
|
+
venues: template.venues,
|
|
69
|
+
});
|
|
54
70
|
}
|
|
55
71
|
|
|
56
72
|
// Default: hold strategy (no trades)
|
|
@@ -64,51 +80,40 @@ async function loadFromFile(filePath: string): Promise<StrategyFunction> {
|
|
|
64
80
|
}
|
|
65
81
|
|
|
66
82
|
try {
|
|
67
|
-
const
|
|
68
|
-
const
|
|
83
|
+
const source = readFileSync(resolved, 'utf8');
|
|
84
|
+
const metadata = extname(resolved) === '.json'
|
|
85
|
+
? strategyMetadataSchema.parse(JSON.parse(source))
|
|
86
|
+
: parseStrategyMetadataModule(source, resolved);
|
|
69
87
|
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
88
|
+
if (metadata.systemPrompt?.trim()) {
|
|
89
|
+
return loadFromPrompt({
|
|
90
|
+
name: metadata.name,
|
|
91
|
+
systemPrompt: metadata.systemPrompt,
|
|
92
|
+
venues: metadata.venues,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
74
95
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const name = typeof mod.name === 'string' ? mod.name : undefined;
|
|
80
|
-
return loadFromPrompt({
|
|
81
|
-
name,
|
|
82
|
-
systemPrompt: mod.systemPrompt,
|
|
83
|
-
venues,
|
|
84
|
-
});
|
|
96
|
+
if (metadata.template?.trim()) {
|
|
97
|
+
const template = getTemplate(metadata.template);
|
|
98
|
+
if (!template) {
|
|
99
|
+
throw new Error(`Unknown strategy template: ${metadata.template}. Available: momentum, value, arbitrage, hold`);
|
|
85
100
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const template = getTemplate(mod.template);
|
|
89
|
-
if (!template) {
|
|
90
|
-
throw new Error(`Unknown strategy template: ${mod.template}. Available: momentum, value, arbitrage, hold`);
|
|
91
|
-
}
|
|
92
|
-
return loadFromCode(template.code);
|
|
101
|
+
if (!template.systemPrompt.trim()) {
|
|
102
|
+
return holdStrategy;
|
|
93
103
|
}
|
|
94
|
-
|
|
95
|
-
|
|
104
|
+
return loadFromPrompt({
|
|
105
|
+
name: template.name,
|
|
106
|
+
systemPrompt: template.systemPrompt,
|
|
107
|
+
venues: metadata.venues?.length ? metadata.venues : template.venues,
|
|
108
|
+
});
|
|
96
109
|
}
|
|
97
110
|
|
|
98
|
-
|
|
111
|
+
throw new Error('Strategy file must define a systemPrompt or template. Executable strategy files are disabled.');
|
|
99
112
|
} catch (err) {
|
|
100
113
|
throw new Error(`Failed to load strategy from ${resolved}: ${(err as Error).message}`);
|
|
101
114
|
}
|
|
102
115
|
}
|
|
103
116
|
|
|
104
|
-
async function loadFromCode(code: string): Promise<StrategyFunction> {
|
|
105
|
-
// Templates return a factory function as a string
|
|
106
|
-
// We wrap it in a module and evaluate
|
|
107
|
-
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
|
108
|
-
const fn = new AsyncFunction('context', code) as StrategyFunction;
|
|
109
|
-
return fn;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
117
|
function loadFromPrompt(config: {
|
|
113
118
|
name?: string;
|
|
114
119
|
systemPrompt: string;
|
|
@@ -124,17 +129,18 @@ function loadFromPrompt(config: {
|
|
|
124
129
|
chain: position.chain,
|
|
125
130
|
}));
|
|
126
131
|
|
|
132
|
+
const allowedVenues = config.venues?.filter(Boolean) ?? [];
|
|
127
133
|
const response = await context.llm.chat([
|
|
128
134
|
{ role: 'system', content: config.systemPrompt },
|
|
129
135
|
{
|
|
130
136
|
role: 'user',
|
|
131
137
|
content: [
|
|
132
138
|
`Strategy: ${config.name || 'Prompt Strategy'}`,
|
|
133
|
-
`Allowed venues: ${
|
|
139
|
+
`Allowed venues: ${allowedVenues.join(', ') || 'none configured'}`,
|
|
134
140
|
`Current prices: ${JSON.stringify(prices)}`,
|
|
135
141
|
`Open positions: ${JSON.stringify(positions)}`,
|
|
136
142
|
`Risk config: ${JSON.stringify(context.config)}`,
|
|
137
|
-
'Return ONLY a JSON array of trade
|
|
143
|
+
'Return ONLY a JSON array of trade intents. Do not include executable code. Do not invent fill IDs.',
|
|
138
144
|
].join('\n'),
|
|
139
145
|
},
|
|
140
146
|
]);
|
|
@@ -142,16 +148,26 @@ function loadFromPrompt(config: {
|
|
|
142
148
|
// Defense-in-depth: scrub any secrets the LLM might echo back before parsing
|
|
143
149
|
const scrubbedContent = scrubSecrets(response.content);
|
|
144
150
|
|
|
145
|
-
const
|
|
146
|
-
if (!
|
|
151
|
+
const jsonPayload = extractJsonArray(scrubbedContent);
|
|
152
|
+
if (!jsonPayload) {
|
|
147
153
|
context.log('Prompt strategy returned a non-JSON response; no signals emitted.');
|
|
148
154
|
return [];
|
|
149
155
|
}
|
|
150
156
|
|
|
151
157
|
try {
|
|
152
|
-
const parsed = promptSignalArraySchema.parse(JSON.parse(
|
|
158
|
+
const parsed = promptSignalArraySchema.parse(JSON.parse(jsonPayload));
|
|
153
159
|
const signals: TradeSignal[] = [];
|
|
154
160
|
for (const signal of parsed) {
|
|
161
|
+
const venue = signal.venue || allowedVenues[0];
|
|
162
|
+
if (!venue) {
|
|
163
|
+
context.log(`Prompt strategy skipped ${signal.symbol}: no allowed venue configured.`);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (allowedVenues.length > 0 && !allowedVenues.includes(venue)) {
|
|
167
|
+
context.log(`Prompt strategy skipped ${signal.symbol}: venue ${venue} is not allowed.`);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
155
171
|
const price = signal.price ?? prices[signal.symbol.toUpperCase()];
|
|
156
172
|
if (!price || price <= 0) {
|
|
157
173
|
context.log(`Prompt strategy skipped ${signal.symbol}: no usable price in response or market cache.`);
|
|
@@ -163,13 +179,13 @@ function loadFromPrompt(config: {
|
|
|
163
179
|
side: signal.side,
|
|
164
180
|
confidence: signal.confidence ?? 0.5,
|
|
165
181
|
reasoning: signal.reasoning,
|
|
166
|
-
venue
|
|
182
|
+
venue,
|
|
167
183
|
chain: signal.chain,
|
|
168
|
-
size: signal.size ??
|
|
184
|
+
size: signal.size ?? 0,
|
|
169
185
|
price,
|
|
170
|
-
fee:
|
|
171
|
-
venueFillId:
|
|
172
|
-
venueTimestamp:
|
|
186
|
+
fee: 0,
|
|
187
|
+
venueFillId: '',
|
|
188
|
+
venueTimestamp: '',
|
|
173
189
|
leverage: signal.leverage,
|
|
174
190
|
orderType: signal.orderType,
|
|
175
191
|
});
|
|
@@ -182,6 +198,64 @@ function loadFromPrompt(config: {
|
|
|
182
198
|
};
|
|
183
199
|
}
|
|
184
200
|
|
|
201
|
+
function extractJsonArray(content: string): string | null {
|
|
202
|
+
const trimmed = content.trim();
|
|
203
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
204
|
+
return trimmed;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const start = trimmed.indexOf('[');
|
|
208
|
+
const end = trimmed.lastIndexOf(']');
|
|
209
|
+
if (start === -1 || end === -1 || end <= start) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
return trimmed.slice(start, end + 1);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function parseStrategyMetadataModule(source: string, filePath: string): z.infer<typeof strategyMetadataSchema> {
|
|
216
|
+
const values: Record<string, unknown> = {};
|
|
217
|
+
let statement = '';
|
|
218
|
+
|
|
219
|
+
for (const rawLine of source.replace(/\r\n/g, '\n').split('\n')) {
|
|
220
|
+
const line = rawLine.trim();
|
|
221
|
+
if (!line || line.startsWith('//')) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
statement = statement ? `${statement}\n${rawLine}` : rawLine;
|
|
226
|
+
if (!line.endsWith(';')) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const match = statement.match(/^\s*export\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*([\s\S]*);\s*$/);
|
|
231
|
+
if (!match) {
|
|
232
|
+
throw new Error(`Unsupported statement in ${filePath}. Strategy files may only export JSON constants.`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const [, name, rawValue] = match;
|
|
236
|
+
if (name === 'code') {
|
|
237
|
+
throw new Error('Raw JavaScript strategy code is disabled. Replace code with systemPrompt.');
|
|
238
|
+
}
|
|
239
|
+
if (!SUPPORTED_FILE_EXPORTS.has(name)) {
|
|
240
|
+
throw new Error(`Unsupported strategy export "${name}".`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
values[name] = JSON.parse(rawValue.trim());
|
|
245
|
+
} catch {
|
|
246
|
+
throw new Error(`Strategy export "${name}" must be a JSON literal.`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
statement = '';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (statement.trim()) {
|
|
253
|
+
throw new Error(`Unterminated export in ${filePath}.`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return strategyMetadataSchema.parse(values);
|
|
257
|
+
}
|
|
258
|
+
|
|
185
259
|
const holdStrategy: StrategyFunction = async (_context: StrategyContext): Promise<TradeSignal[]> => {
|
|
186
260
|
return []; // No trades — hold position
|
|
187
261
|
};
|
|
@@ -18,23 +18,6 @@ Rules:
|
|
|
18
18
|
- Consider volume as confirmation of momentum
|
|
19
19
|
|
|
20
20
|
Return a JSON array of trade signals.`,
|
|
21
|
-
code: `
|
|
22
|
-
const prices = context.market.getPrices();
|
|
23
|
-
const positions = context.position.openPositions;
|
|
24
|
-
|
|
25
|
-
const messages = [
|
|
26
|
-
{ role: 'system', content: 'You are a momentum trading agent. Analyze market data and return trade signals as a JSON array. Each signal: { symbol, side: "buy"|"sell", confidence: 0-1, reasoning }. Return [] if no opportunities.' },
|
|
27
|
-
{ role: 'user', content: 'Current prices: ' + JSON.stringify(prices) + '\\nOpen positions: ' + JSON.stringify(positions.map(p => p.token)) + '\\nAnalyze for momentum opportunities.' }
|
|
28
|
-
];
|
|
29
|
-
|
|
30
|
-
const response = await context.llm.chat(messages);
|
|
31
|
-
try {
|
|
32
|
-
const signals = JSON.parse(response.content);
|
|
33
|
-
return Array.isArray(signals) ? signals : [];
|
|
34
|
-
} catch {
|
|
35
|
-
return [];
|
|
36
|
-
}
|
|
37
|
-
`,
|
|
38
21
|
},
|
|
39
22
|
{
|
|
40
23
|
id: 'value',
|
|
@@ -53,23 +36,6 @@ Rules:
|
|
|
53
36
|
- Diversify across sectors
|
|
54
37
|
|
|
55
38
|
Return a JSON array of trade signals.`,
|
|
56
|
-
code: `
|
|
57
|
-
const prices = context.market.getPrices();
|
|
58
|
-
const positions = context.position.openPositions;
|
|
59
|
-
|
|
60
|
-
const messages = [
|
|
61
|
-
{ role: 'system', content: 'You are a value investing agent. Identify undervalued tokens. Return trade signals as JSON array: { symbol, side: "buy"|"sell", confidence: 0-1, reasoning }. Return [] if nothing is compelling.' },
|
|
62
|
-
{ role: 'user', content: 'Current prices: ' + JSON.stringify(prices) + '\\nPositions: ' + JSON.stringify(positions.map(p => ({ token: p.token, entry: p.costBasisPerUnit }))) + '\\nLook for value opportunities.' }
|
|
63
|
-
];
|
|
64
|
-
|
|
65
|
-
const response = await context.llm.chat(messages);
|
|
66
|
-
try {
|
|
67
|
-
const signals = JSON.parse(response.content);
|
|
68
|
-
return Array.isArray(signals) ? signals : [];
|
|
69
|
-
} catch {
|
|
70
|
-
return [];
|
|
71
|
-
}
|
|
72
|
-
`,
|
|
73
39
|
},
|
|
74
40
|
{
|
|
75
41
|
id: 'arbitrage',
|
|
@@ -87,22 +53,6 @@ Rules:
|
|
|
87
53
|
- Monitor cross-chain opportunities (CEX vs DEX spreads)
|
|
88
54
|
|
|
89
55
|
Return a JSON array of trade signals.`,
|
|
90
|
-
code: `
|
|
91
|
-
const prices = context.market.getPrices();
|
|
92
|
-
|
|
93
|
-
const messages = [
|
|
94
|
-
{ role: 'system', content: 'You are an arbitrage agent. Find price discrepancies. Return trade signals as JSON array: { symbol, side: "buy"|"sell", venue, confidence: 0-1, reasoning }. Return [] if no arb opportunities.' },
|
|
95
|
-
{ role: 'user', content: 'Market prices: ' + JSON.stringify(prices) + '\\nScan for cross-venue arbitrage opportunities.' }
|
|
96
|
-
];
|
|
97
|
-
|
|
98
|
-
const response = await context.llm.chat(messages);
|
|
99
|
-
try {
|
|
100
|
-
const signals = JSON.parse(response.content);
|
|
101
|
-
return Array.isArray(signals) ? signals : [];
|
|
102
|
-
} catch {
|
|
103
|
-
return [];
|
|
104
|
-
}
|
|
105
|
-
`,
|
|
106
56
|
},
|
|
107
57
|
{
|
|
108
58
|
id: 'hold',
|
|
@@ -112,7 +62,6 @@ try {
|
|
|
112
62
|
venues: [],
|
|
113
63
|
riskLevel: 'conservative',
|
|
114
64
|
systemPrompt: '',
|
|
115
|
-
code: `return [];`,
|
|
116
65
|
},
|
|
117
66
|
];
|
|
118
67
|
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { loadStrategy } from '../src/strategy/loader.js';
|
|
7
|
+
import type { StrategyContext } from '@exagent/sdk';
|
|
8
|
+
|
|
9
|
+
function createContext(llmContent: string, prices: Record<string, number> = { ETH: 3000 }): StrategyContext {
|
|
10
|
+
const logs: string[] = [];
|
|
11
|
+
return {
|
|
12
|
+
llm: {
|
|
13
|
+
async chat() {
|
|
14
|
+
return { content: llmContent };
|
|
15
|
+
},
|
|
16
|
+
getMetadata() {
|
|
17
|
+
return { provider: 'test', model: 'test' };
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
market: {
|
|
21
|
+
getPrice(symbol: string) {
|
|
22
|
+
return prices[symbol] ?? prices[symbol.toUpperCase()];
|
|
23
|
+
},
|
|
24
|
+
getPrices() {
|
|
25
|
+
return prices;
|
|
26
|
+
},
|
|
27
|
+
getOHLCV() {
|
|
28
|
+
return [];
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
position: {
|
|
32
|
+
openPositions: [],
|
|
33
|
+
totalUnrealizedPnL: 0,
|
|
34
|
+
totalRealizedPnL: 0,
|
|
35
|
+
},
|
|
36
|
+
store: {
|
|
37
|
+
get() {
|
|
38
|
+
return undefined;
|
|
39
|
+
},
|
|
40
|
+
set() {
|
|
41
|
+
// no-op
|
|
42
|
+
},
|
|
43
|
+
delete() {
|
|
44
|
+
// no-op
|
|
45
|
+
},
|
|
46
|
+
keys() {
|
|
47
|
+
return [];
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
config: {
|
|
51
|
+
mode: 'paper',
|
|
52
|
+
timeHorizon: 'swing',
|
|
53
|
+
maxPositionSizeBps: 2000,
|
|
54
|
+
maxDailyLossBps: 500,
|
|
55
|
+
maxConcurrentPositions: 5,
|
|
56
|
+
tradingIntervalMs: 60000,
|
|
57
|
+
maxSlippageBps: 100,
|
|
58
|
+
minTradeValueUSD: 10,
|
|
59
|
+
initialCapitalUSD: 10000,
|
|
60
|
+
},
|
|
61
|
+
log(message: string) {
|
|
62
|
+
logs.push(message);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
test('loadStrategy rejects raw JavaScript code configs', async () => {
|
|
68
|
+
await assert.rejects(
|
|
69
|
+
() => loadStrategy({ code: 'globalThis.stoleSecrets = true; return [];' }),
|
|
70
|
+
/Raw JavaScript strategy code is disabled/,
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('template strategies run through prompt-backed validation', async () => {
|
|
75
|
+
const strategy = await loadStrategy({ template: 'momentum' });
|
|
76
|
+
const signals = await strategy(createContext(JSON.stringify([
|
|
77
|
+
{ symbol: 'ETH', side: 'buy', venue: 'hyperliquid_perp', confidence: 0.8 },
|
|
78
|
+
])));
|
|
79
|
+
|
|
80
|
+
assert.equal(signals.length, 1);
|
|
81
|
+
assert.equal(signals[0].symbol, 'ETH');
|
|
82
|
+
assert.equal(signals[0].venue, 'hyperliquid_perp');
|
|
83
|
+
assert.equal(signals[0].price, 3000);
|
|
84
|
+
assert.equal(signals[0].venueFillId, '');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('prompt strategies drop venues outside the configured strategy scope', async () => {
|
|
88
|
+
const strategy = await loadStrategy({
|
|
89
|
+
prompt: {
|
|
90
|
+
name: 'Scoped Prompt',
|
|
91
|
+
systemPrompt: 'Return scoped trade intents.',
|
|
92
|
+
venues: ['hyperliquid_perp'],
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const signals = await strategy(createContext(JSON.stringify([
|
|
97
|
+
{ symbol: 'ETH', side: 'buy', venue: 'polymarket', confidence: 0.9 },
|
|
98
|
+
{ symbol: 'ETH', side: 'buy', venue: 'hyperliquid_perp', confidence: 0.9 },
|
|
99
|
+
])));
|
|
100
|
+
|
|
101
|
+
assert.equal(signals.length, 1);
|
|
102
|
+
assert.equal(signals[0].venue, 'hyperliquid_perp');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('strategy metadata files are parsed as data, not executable modules', async () => {
|
|
106
|
+
const dir = mkdtempSync(join(tmpdir(), 'exagent-strategy-'));
|
|
107
|
+
const strategyPath = join(dir, 'strategy.ts');
|
|
108
|
+
|
|
109
|
+
writeFileSync(strategyPath, [
|
|
110
|
+
'// Generated by Exagent',
|
|
111
|
+
'export const name = "Prompt File";',
|
|
112
|
+
'export const venues = ["hyperliquid_perp"];',
|
|
113
|
+
'export const systemPrompt = "Return trade intents.";'
|
|
114
|
+
].join('\n'));
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const strategy = await loadStrategy({ file: strategyPath });
|
|
118
|
+
const signals = await strategy(createContext(JSON.stringify([
|
|
119
|
+
{ symbol: 'ETH', side: 'long', confidence: 0.7 },
|
|
120
|
+
])));
|
|
121
|
+
|
|
122
|
+
assert.equal(signals.length, 1);
|
|
123
|
+
assert.equal(signals[0].venue, 'hyperliquid_perp');
|
|
124
|
+
} finally {
|
|
125
|
+
rmSync(dir, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('strategy files cannot export code or arbitrary statements', async () => {
|
|
130
|
+
const dir = mkdtempSync(join(tmpdir(), 'exagent-strategy-'));
|
|
131
|
+
const codeExportPath = join(dir, 'code-strategy.ts');
|
|
132
|
+
const statementPath = join(dir, 'statement-strategy.ts');
|
|
133
|
+
|
|
134
|
+
writeFileSync(codeExportPath, 'export const code = "return [];";\n');
|
|
135
|
+
writeFileSync(statementPath, 'export const systemPrompt = "Return []";\nglobalThis.compromised = true;\n');
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
await assert.rejects(
|
|
139
|
+
() => loadStrategy({ file: codeExportPath }),
|
|
140
|
+
/Raw JavaScript strategy code is disabled/,
|
|
141
|
+
);
|
|
142
|
+
await assert.rejects(
|
|
143
|
+
() => loadStrategy({ file: statementPath }),
|
|
144
|
+
/Unsupported statement/,
|
|
145
|
+
);
|
|
146
|
+
assert.equal((globalThis as Record<string, unknown>).compromised, undefined);
|
|
147
|
+
} finally {
|
|
148
|
+
rmSync(dir, { recursive: true, force: true });
|
|
149
|
+
}
|
|
150
|
+
});
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @exagent/agent@0.3.0 build /Users/graydon/Codebase Repertoire/Exagent/packages/agent
|
|
3
|
-
> tsup src/index.ts src/cli.ts --format esm --dts
|
|
4
|
-
|
|
5
|
-
CLI Building entry: src/cli.ts, src/index.ts
|
|
6
|
-
CLI Using tsconfig: tsconfig.json
|
|
7
|
-
CLI tsup v8.5.1
|
|
8
|
-
CLI Target: es2022
|
|
9
|
-
ESM Build start
|
|
10
|
-
ESM dist/cli.js 9.50 KB
|
|
11
|
-
ESM dist/index.js 1.75 KB
|
|
12
|
-
ESM dist/chunk-SVFTC5V2.js 201.37 KB
|
|
13
|
-
ESM ⚡️ Build success in 34ms
|
|
14
|
-
DTS Build start
|
|
15
|
-
DTS ⚡️ Build success in 8241ms
|
|
16
|
-
DTS dist/cli.d.ts 20.00 B
|
|
17
|
-
DTS dist/index.d.ts 41.02 KB
|