@exagent/agent 0.3.5 → 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 +39 -97
- package/dist/index.js +1 -1
- package/package.json +18 -18
- package/src/cli.ts +18 -12
- 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 -100
- package/src/prediction/client.ts +11 -4
- package/src/runtime.ts +3 -3
- package/src/setup.ts +18 -10
- 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/llm-providers.ts
CHANGED
|
@@ -1,100 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface LlmProvider {
|
|
12
|
-
id: string;
|
|
13
|
-
label: string;
|
|
14
|
-
models: LlmModel[];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export const LLM_PROVIDERS: LlmProvider[] = [
|
|
18
|
-
{
|
|
19
|
-
id: 'openai',
|
|
20
|
-
label: 'OpenAI',
|
|
21
|
-
models: [
|
|
22
|
-
{ id: 'gpt-5.2', label: 'GPT-5.2' },
|
|
23
|
-
{ id: 'gpt-5.2-pro', label: 'GPT-5.2 Pro' },
|
|
24
|
-
{ id: 'gpt-5-mini', label: 'GPT-5 Mini' },
|
|
25
|
-
{ id: 'gpt-5-nano', label: 'GPT-5 Nano' },
|
|
26
|
-
{ id: 'gpt-4o', label: 'GPT-4o' },
|
|
27
|
-
{ id: 'gpt-4o-mini', label: 'GPT-4o Mini' },
|
|
28
|
-
],
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
id: 'anthropic',
|
|
32
|
-
label: 'Anthropic',
|
|
33
|
-
models: [
|
|
34
|
-
{ id: 'claude-opus-4-6', label: 'Claude Opus 4.6' },
|
|
35
|
-
{ id: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6' },
|
|
36
|
-
{ id: 'claude-haiku-4-5', label: 'Claude Haiku 4.5' },
|
|
37
|
-
],
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
id: 'google',
|
|
41
|
-
label: 'Google',
|
|
42
|
-
models: [
|
|
43
|
-
{ id: 'gemini-3-pro', label: 'Gemini 3 Pro' },
|
|
44
|
-
{ id: 'gemini-3-flash', label: 'Gemini 3 Flash' },
|
|
45
|
-
{ id: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' },
|
|
46
|
-
{ id: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' },
|
|
47
|
-
{ id: 'gemini-2.5-flash-lite', label: 'Gemini 2.5 Flash Lite' },
|
|
48
|
-
],
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
id: 'deepseek',
|
|
52
|
-
label: 'DeepSeek',
|
|
53
|
-
models: [
|
|
54
|
-
{ id: 'deepseek-chat', label: 'DeepSeek Chat' },
|
|
55
|
-
{ id: 'deepseek-reasoner', label: 'DeepSeek Reasoner' },
|
|
56
|
-
],
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
id: 'mistral',
|
|
60
|
-
label: 'Mistral',
|
|
61
|
-
models: [
|
|
62
|
-
{ id: 'mistral-large-latest', label: 'Mistral Large' },
|
|
63
|
-
{ id: 'mistral-small-latest', label: 'Mistral Small' },
|
|
64
|
-
],
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
id: 'groq',
|
|
68
|
-
label: 'Groq',
|
|
69
|
-
models: [
|
|
70
|
-
{ id: 'llama-3.3-70b-versatile', label: 'Llama 3.3 70B' },
|
|
71
|
-
{ id: 'llama-3.1-8b-instant', label: 'Llama 3.1 8B' },
|
|
72
|
-
{ id: 'mixtral-8x7b-32768', label: 'Mixtral 8x7B' },
|
|
73
|
-
],
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
id: 'together',
|
|
77
|
-
label: 'Together',
|
|
78
|
-
models: [
|
|
79
|
-
{ id: 'meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo', label: 'Llama 3.1 70B' },
|
|
80
|
-
{ id: 'meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo', label: 'Llama 3.1 8B' },
|
|
81
|
-
],
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
id: 'ollama',
|
|
85
|
-
label: 'Ollama (local)',
|
|
86
|
-
models: [
|
|
87
|
-
{ id: 'llama3.1', label: 'Llama 3.1' },
|
|
88
|
-
{ id: 'mistral', label: 'Mistral' },
|
|
89
|
-
{ id: 'custom', label: 'Custom (type model name)' },
|
|
90
|
-
],
|
|
91
|
-
},
|
|
92
|
-
];
|
|
93
|
-
|
|
94
|
-
export function getProvider(id: string): LlmProvider | undefined {
|
|
95
|
-
return LLM_PROVIDERS.find((p) => p.id === id);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function getProviderIds(): string[] {
|
|
99
|
-
return LLM_PROVIDERS.map((p) => p.id);
|
|
100
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
LLM_PROVIDERS,
|
|
3
|
+
getDefaultModel,
|
|
4
|
+
getProvider,
|
|
5
|
+
getProviderIds,
|
|
6
|
+
providerRequiresApiKey,
|
|
7
|
+
} from '@exagent/sdk';
|
|
8
|
+
export type { LlmModel, LlmProvider } from '@exagent/sdk';
|
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
|
-
import { LLM_PROVIDERS, getProvider } from './llm-providers.js';
|
|
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,7 +161,9 @@ 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
|
}
|
|
@@ -182,7 +184,7 @@ async function setupLlm(
|
|
|
182
184
|
const providerInfo = getProvider(provider);
|
|
183
185
|
const modelOptions = providerInfo
|
|
184
186
|
? providerInfo.models.map(m => ({ value: m.id, label: m.label }))
|
|
185
|
-
: [{ value: defaultModel || '
|
|
187
|
+
: [{ value: defaultModel || getDefaultModel('openai'), label: defaultModel || getDefaultModel('openai') }];
|
|
186
188
|
const selectedModel = await clack.select({
|
|
187
189
|
message: 'LLM model:',
|
|
188
190
|
options: modelOptions,
|
|
@@ -191,12 +193,18 @@ async function setupLlm(
|
|
|
191
193
|
if (clack.isCancel(selectedModel)) cancelled();
|
|
192
194
|
const model = selectedModel;
|
|
193
195
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
+
}
|
|
200
208
|
|
|
201
209
|
printDone('LLM configured');
|
|
202
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
|
|