@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.
- package/dist/chunk-7UGLJO6W.js +6392 -0
- package/dist/chunk-EHAOPCTJ.js +6406 -0
- package/dist/chunk-FGMXTW5I.js +6540 -0
- package/dist/chunk-GYYW4EKM.js +6756 -0
- package/dist/chunk-IVA2SCSN.js +6756 -0
- package/dist/chunk-JHXCSGPC.js +6352 -0
- package/dist/chunk-V6O4UXVN.js +6345 -0
- package/dist/chunk-WTECTX2Z.js +6345 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +24 -2
- package/dist/index.js +1 -1
- package/package.json +12 -9
- package/src/bridge/across.ts +0 -240
- package/src/bridge/bridge-manager.ts +0 -87
- package/src/bridge/index.ts +0 -9
- package/src/bridge/types.ts +0 -77
- package/src/chains.ts +0 -105
- package/src/cli.ts +0 -250
- package/src/config.ts +0 -502
- package/src/diagnostics.ts +0 -335
- package/src/index.ts +0 -98
- package/src/llm/anthropic.ts +0 -63
- package/src/llm/base.ts +0 -264
- package/src/llm/deepseek.ts +0 -48
- package/src/llm/google.ts +0 -63
- package/src/llm/groq.ts +0 -48
- package/src/llm/index.ts +0 -42
- package/src/llm/mistral.ts +0 -48
- package/src/llm/ollama.ts +0 -52
- package/src/llm/openai.ts +0 -94
- package/src/llm/together.ts +0 -48
- package/src/llm-providers.ts +0 -8
- package/src/logger.ts +0 -137
- package/src/paper/executor.ts +0 -201
- package/src/paper/index.ts +0 -1
- package/src/perp/client.ts +0 -200
- package/src/perp/index.ts +0 -12
- package/src/perp/msgpack.ts +0 -272
- package/src/perp/orders.ts +0 -234
- package/src/perp/positions.ts +0 -126
- package/src/perp/signer.ts +0 -277
- package/src/perp/types.ts +0 -192
- package/src/perp/websocket.ts +0 -274
- package/src/position-tracker.ts +0 -243
- package/src/prediction/client.ts +0 -288
- package/src/prediction/index.ts +0 -3
- package/src/prediction/order-manager.ts +0 -297
- package/src/prediction/types.ts +0 -151
- package/src/relay.ts +0 -254
- package/src/runtime.ts +0 -1755
- package/src/scrub-secrets.ts +0 -39
- package/src/setup.ts +0 -392
- package/src/signal.ts +0 -212
- package/src/spot/aerodrome.ts +0 -158
- package/src/spot/client.ts +0 -138
- package/src/spot/index.ts +0 -11
- package/src/spot/swap-manager.ts +0 -219
- package/src/spot/types.ts +0 -203
- package/src/spot/uniswap.ts +0 -150
- package/src/store.ts +0 -50
- package/src/strategy/index.ts +0 -2
- package/src/strategy/loader.ts +0 -265
- package/src/strategy/templates.ts +0 -74
- package/src/trading/index.ts +0 -2
- package/src/trading/market.ts +0 -120
- package/src/trading/risk.ts +0 -107
- package/src/ui.ts +0 -75
- package/test/strategy-loader.test.ts +0 -150
- package/tsconfig.json +0 -8
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import type { StrategyTemplate } from '@exagent/sdk';
|
|
2
|
-
|
|
3
|
-
const templates: StrategyTemplate[] = [
|
|
4
|
-
{
|
|
5
|
-
id: 'momentum',
|
|
6
|
-
name: 'Momentum Trader',
|
|
7
|
-
description: 'Identifies tokens with strong upward price momentum and rides the trend. Uses LLM to analyze price action and market sentiment.',
|
|
8
|
-
category: 'momentum',
|
|
9
|
-
venues: ['hyperliquid_perp', 'hyperliquid_spot'],
|
|
10
|
-
riskLevel: 'moderate',
|
|
11
|
-
systemPrompt: `You are a momentum trading agent. Analyze the provided market data and identify tokens with strong upward momentum.
|
|
12
|
-
|
|
13
|
-
Rules:
|
|
14
|
-
- Only trade tokens with clear directional momentum (avoid choppy markets)
|
|
15
|
-
- Use trailing stops to protect gains
|
|
16
|
-
- Size positions based on conviction (higher confidence = larger position)
|
|
17
|
-
- Cut losses quickly if momentum reverses
|
|
18
|
-
- Consider volume as confirmation of momentum
|
|
19
|
-
|
|
20
|
-
Return a JSON array of trade signals.`,
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
id: 'value',
|
|
24
|
-
name: 'Value Investor',
|
|
25
|
-
description: 'Identifies undervalued tokens based on fundamental analysis. Buys dips and holds for mean reversion.',
|
|
26
|
-
category: 'value',
|
|
27
|
-
venues: ['hyperliquid_spot'],
|
|
28
|
-
riskLevel: 'conservative',
|
|
29
|
-
systemPrompt: `You are a value investing agent. Identify tokens trading below their intrinsic value.
|
|
30
|
-
|
|
31
|
-
Rules:
|
|
32
|
-
- Focus on established tokens with strong fundamentals
|
|
33
|
-
- Buy on significant dips (>10% from recent highs)
|
|
34
|
-
- Hold positions longer (swing/position timeframe)
|
|
35
|
-
- Avoid chasing pumps
|
|
36
|
-
- Diversify across sectors
|
|
37
|
-
|
|
38
|
-
Return a JSON array of trade signals.`,
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
id: 'arbitrage',
|
|
42
|
-
name: 'Arbitrage Hunter',
|
|
43
|
-
description: 'Scans for price discrepancies across venues and chains. Executes quickly to capture spreads.',
|
|
44
|
-
category: 'arbitrage',
|
|
45
|
-
venues: ['hyperliquid_perp', 'hyperliquid_spot', 'uniswap', 'aerodrome'],
|
|
46
|
-
riskLevel: 'aggressive',
|
|
47
|
-
systemPrompt: `You are an arbitrage agent. Find price discrepancies between venues.
|
|
48
|
-
|
|
49
|
-
Rules:
|
|
50
|
-
- Speed is critical — execute quickly before spreads close
|
|
51
|
-
- Account for fees and slippage in profitability calculations
|
|
52
|
-
- Only trade when net profit > 0.5% after all costs
|
|
53
|
-
- Monitor cross-chain opportunities (CEX vs DEX spreads)
|
|
54
|
-
|
|
55
|
-
Return a JSON array of trade signals.`,
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
id: 'hold',
|
|
59
|
-
name: 'Hold (No Trading)',
|
|
60
|
-
description: 'Passive strategy that makes no trades. Useful for monitoring only.',
|
|
61
|
-
category: 'custom',
|
|
62
|
-
venues: [],
|
|
63
|
-
riskLevel: 'conservative',
|
|
64
|
-
systemPrompt: '',
|
|
65
|
-
},
|
|
66
|
-
];
|
|
67
|
-
|
|
68
|
-
export function getTemplate(id: string): StrategyTemplate | undefined {
|
|
69
|
-
return templates.find(t => t.id === id);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function listTemplates(): StrategyTemplate[] {
|
|
73
|
-
return [...templates];
|
|
74
|
-
}
|
package/src/trading/index.ts
DELETED
package/src/trading/market.ts
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import type { MarketData, OHLCV } from '@exagent/sdk';
|
|
2
|
-
|
|
3
|
-
export class MarketDataService implements MarketData {
|
|
4
|
-
private prices: Record<string, number> = {};
|
|
5
|
-
private lastFetch = 0;
|
|
6
|
-
private cacheTTL: number;
|
|
7
|
-
private apiUrl: string | null;
|
|
8
|
-
private ohlcvCache: Map<string, { data: OHLCV[]; fetchedAt: number }> = new Map();
|
|
9
|
-
|
|
10
|
-
constructor(cacheTTLMs: number = 30000, apiUrl?: string) {
|
|
11
|
-
this.cacheTTL = cacheTTLMs;
|
|
12
|
-
this.apiUrl = apiUrl || null;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
async refreshPrices(symbols: string[]): Promise<Record<string, number>> {
|
|
16
|
-
if (symbols.length === 0) return this.prices;
|
|
17
|
-
|
|
18
|
-
const now = Date.now();
|
|
19
|
-
if (now - this.lastFetch < this.cacheTTL && Object.keys(this.prices).length > 0) {
|
|
20
|
-
return this.prices;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Try API first (if relay URL is configured)
|
|
24
|
-
if (this.apiUrl) {
|
|
25
|
-
try {
|
|
26
|
-
const res = await fetch(`${this.apiUrl}/v1/prices`);
|
|
27
|
-
if (res.ok) {
|
|
28
|
-
const data = await res.json() as Record<string, number>;
|
|
29
|
-
for (const [symbol, price] of Object.entries(data)) {
|
|
30
|
-
this.prices[symbol.toUpperCase()] = price;
|
|
31
|
-
}
|
|
32
|
-
this.lastFetch = now;
|
|
33
|
-
return this.prices;
|
|
34
|
-
}
|
|
35
|
-
} catch {
|
|
36
|
-
// Fall through to CoinGecko
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Fallback: CoinGecko (free, no API key)
|
|
41
|
-
try {
|
|
42
|
-
const ids = symbols.map(s => s.toLowerCase()).join(',');
|
|
43
|
-
const res = await fetch(
|
|
44
|
-
`https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd`,
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
if (res.ok) {
|
|
48
|
-
const data = await res.json() as Record<string, { usd: number }>;
|
|
49
|
-
for (const [id, price] of Object.entries(data)) {
|
|
50
|
-
this.prices[id.toUpperCase()] = price.usd;
|
|
51
|
-
}
|
|
52
|
-
this.lastFetch = now;
|
|
53
|
-
}
|
|
54
|
-
} catch (err) {
|
|
55
|
-
console.warn('[market] Price fetch failed:', (err as Error).message);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return this.prices;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
getPrice(symbol: string): number | undefined {
|
|
62
|
-
return this.prices[symbol.toUpperCase()];
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
getPrices(): Record<string, number> {
|
|
66
|
-
return { ...this.prices };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
setPrice(symbol: string, price: number): void {
|
|
70
|
-
this.prices[symbol.toUpperCase()] = price;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
setPrices(prices: Record<string, number>): void {
|
|
74
|
-
for (const [symbol, price] of Object.entries(prices)) {
|
|
75
|
-
this.prices[symbol.toUpperCase()] = price;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
getOHLCV(symbol: string, timeframe: string): OHLCV[] {
|
|
80
|
-
const cacheKey = `${symbol.toUpperCase()}:${timeframe}`;
|
|
81
|
-
const cached = this.ohlcvCache.get(cacheKey);
|
|
82
|
-
if (cached && Date.now() - cached.fetchedAt < this.cacheTTL) {
|
|
83
|
-
return cached.data;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Trigger async fetch (non-blocking) — returns cached or empty on first call
|
|
87
|
-
this.fetchOHLCV(symbol, timeframe).catch(() => {});
|
|
88
|
-
return cached?.data || [];
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
private async fetchOHLCV(symbol: string, timeframe: string): Promise<void> {
|
|
92
|
-
const cacheKey = `${symbol.toUpperCase()}:${timeframe}`;
|
|
93
|
-
|
|
94
|
-
// Try API first
|
|
95
|
-
if (this.apiUrl) {
|
|
96
|
-
try {
|
|
97
|
-
const res = await fetch(
|
|
98
|
-
`${this.apiUrl}/v1/prices/${symbol.toUpperCase()}/ohlcv?interval=${timeframe}&limit=100`,
|
|
99
|
-
);
|
|
100
|
-
if (res.ok) {
|
|
101
|
-
const bars = await res.json() as Array<{ t: number; o: number; h: number; l: number; c: number; v: number }>;
|
|
102
|
-
const ohlcv: OHLCV[] = bars.map((b) => ({
|
|
103
|
-
timestamp: b.t,
|
|
104
|
-
open: b.o,
|
|
105
|
-
high: b.h,
|
|
106
|
-
low: b.l,
|
|
107
|
-
close: b.c,
|
|
108
|
-
volume: b.v,
|
|
109
|
-
}));
|
|
110
|
-
this.ohlcvCache.set(cacheKey, { data: ohlcv, fetchedAt: Date.now() });
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
} catch {
|
|
114
|
-
// Fall through
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// No fallback for OHLCV — requires API
|
|
119
|
-
}
|
|
120
|
-
}
|
package/src/trading/risk.ts
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import type { TradeSignal, RiskParams, MarketData } from '@exagent/sdk';
|
|
2
|
-
|
|
3
|
-
export class RiskManager {
|
|
4
|
-
private params: RiskParams;
|
|
5
|
-
private dailyPnL = 0;
|
|
6
|
-
private dailyFees = 0;
|
|
7
|
-
private lastResetDate: string = '';
|
|
8
|
-
private initialCapitalUSD: number;
|
|
9
|
-
|
|
10
|
-
constructor(params: RiskParams, initialCapitalUSD: number = 10000) {
|
|
11
|
-
this.params = params;
|
|
12
|
-
this.initialCapitalUSD = initialCapitalUSD;
|
|
13
|
-
this.resetIfNewDay();
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
filterSignals(signals: TradeSignal[], market: MarketData, openPositionCount: number): TradeSignal[] {
|
|
17
|
-
this.resetIfNewDay();
|
|
18
|
-
|
|
19
|
-
if (this.isDailyLossLimitHit()) {
|
|
20
|
-
console.log('[risk] Daily loss limit hit — blocking all trades');
|
|
21
|
-
return [];
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return signals.filter(signal => this.validateSignal(signal, market, openPositionCount));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
private validateSignal(signal: TradeSignal, market: MarketData, openPositionCount: number): boolean {
|
|
28
|
-
// Confidence threshold
|
|
29
|
-
const threshold = this.params.confidenceThreshold ?? 0.5;
|
|
30
|
-
if (signal.confidence !== undefined && signal.confidence < threshold) {
|
|
31
|
-
console.log(`[risk] Blocked ${signal.symbol}: confidence ${signal.confidence} < ${threshold}`);
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Position size limit — use leverage-adjusted notional exposure
|
|
36
|
-
// A 10x leveraged $5k position = $50k notional exposure
|
|
37
|
-
const leverage = signal.leverage || 1;
|
|
38
|
-
const tradeValue = signal.size * signal.price * leverage;
|
|
39
|
-
const maxPositionValue = (this.params.maxPositionSizeBps / 10000) * this.initialCapitalUSD;
|
|
40
|
-
if (tradeValue > maxPositionValue) {
|
|
41
|
-
console.log(`[risk] Blocked ${signal.symbol}: notional $${tradeValue.toFixed(0)} (${leverage}x leverage) > max $${maxPositionValue.toFixed(0)}`);
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Min trade value
|
|
46
|
-
if (tradeValue < this.params.minTradeValueUSD) {
|
|
47
|
-
console.log(`[risk] Blocked ${signal.symbol}: trade $${tradeValue.toFixed(2)} < min $${this.params.minTradeValueUSD}`);
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Max concurrent positions (only for buys/longs)
|
|
52
|
-
if ((signal.side === 'buy' || signal.side === 'long') && openPositionCount >= this.params.maxConcurrentPositions) {
|
|
53
|
-
console.log(`[risk] Blocked ${signal.symbol}: ${openPositionCount} positions >= max ${this.params.maxConcurrentPositions}`);
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Max slippage
|
|
58
|
-
if (signal.orderType === 'market') {
|
|
59
|
-
const currentPrice = market.getPrice(signal.symbol);
|
|
60
|
-
if (currentPrice) {
|
|
61
|
-
const slippageBps = Math.abs(signal.price - currentPrice) / currentPrice * 10000;
|
|
62
|
-
if (slippageBps > this.params.maxSlippageBps) {
|
|
63
|
-
console.log(`[risk] Blocked ${signal.symbol}: slippage ${slippageBps.toFixed(0)}bps > max ${this.params.maxSlippageBps}bps`);
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
updateParams(updates: Record<string, number>): void {
|
|
73
|
-
if (updates.maxPositionSizeBps !== undefined) this.params.maxPositionSizeBps = updates.maxPositionSizeBps;
|
|
74
|
-
if (updates.maxDailyLossBps !== undefined) this.params.maxDailyLossBps = updates.maxDailyLossBps;
|
|
75
|
-
if (updates.maxConcurrentPositions !== undefined) this.params.maxConcurrentPositions = updates.maxConcurrentPositions;
|
|
76
|
-
if (updates.maxSlippageBps !== undefined) this.params.maxSlippageBps = updates.maxSlippageBps;
|
|
77
|
-
if (updates.minTradeValueUSD !== undefined) this.params.minTradeValueUSD = updates.minTradeValueUSD;
|
|
78
|
-
console.log('[risk] Params updated:', JSON.stringify(this.params));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
recordTrade(pnl: number, fee: number): void {
|
|
82
|
-
this.dailyPnL += pnl;
|
|
83
|
-
this.dailyFees += fee;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
isDailyLossLimitHit(): boolean {
|
|
87
|
-
const limit = (this.params.maxDailyLossBps / 10000) * this.initialCapitalUSD;
|
|
88
|
-
return this.dailyPnL < -limit;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
getDailyPnL(): number {
|
|
92
|
-
return this.dailyPnL;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
getDailyLossLimit(): number {
|
|
96
|
-
return (this.params.maxDailyLossBps / 10000) * this.initialCapitalUSD;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
private resetIfNewDay(): void {
|
|
100
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
101
|
-
if (today !== this.lastResetDate) {
|
|
102
|
-
this.dailyPnL = 0;
|
|
103
|
-
this.dailyFees = 0;
|
|
104
|
-
this.lastResetDate = today;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
package/src/ui.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import figlet from 'figlet';
|
|
2
|
-
import gradient from 'gradient-string';
|
|
3
|
-
import boxen from 'boxen';
|
|
4
|
-
import pc from 'picocolors';
|
|
5
|
-
import { createRequire } from 'node:module';
|
|
6
|
-
|
|
7
|
-
// Brand gradient: Blue → Indigo → Violet (from Exagent design system)
|
|
8
|
-
const brandGradient: (text: string) => string = gradient(['#3B82F6', '#6366F1', '#7C3AED']);
|
|
9
|
-
|
|
10
|
-
// Secondary gradient: Cyan → Blue
|
|
11
|
-
const accentGradient: (text: string) => string = gradient(['#22D3EE', '#3B82F6']);
|
|
12
|
-
|
|
13
|
-
function getVersion(): string {
|
|
14
|
-
try {
|
|
15
|
-
const require = createRequire(import.meta.url);
|
|
16
|
-
const pkg = require('../package.json');
|
|
17
|
-
return pkg.version || '0.0.0';
|
|
18
|
-
} catch {
|
|
19
|
-
return '0.0.0';
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function printBanner(): void {
|
|
24
|
-
const art = figlet.textSync('EXAGENT', {
|
|
25
|
-
font: 'Small',
|
|
26
|
-
horizontalLayout: 'default',
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
console.log();
|
|
30
|
-
console.log(brandGradient(art));
|
|
31
|
-
console.log(pc.dim(` v${getVersion()}`));
|
|
32
|
-
console.log();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function printSuccess(title: string, lines: string[]): void {
|
|
36
|
-
const body = [
|
|
37
|
-
'',
|
|
38
|
-
pc.bold(pc.white(title)),
|
|
39
|
-
'',
|
|
40
|
-
...lines.map(l => ` ${l}`),
|
|
41
|
-
'',
|
|
42
|
-
].join('\n');
|
|
43
|
-
|
|
44
|
-
console.log();
|
|
45
|
-
console.log(boxen(body, {
|
|
46
|
-
padding: { top: 0, bottom: 0, left: 2, right: 2 },
|
|
47
|
-
borderColor: '#3B82F6',
|
|
48
|
-
borderStyle: 'round',
|
|
49
|
-
dimBorder: false,
|
|
50
|
-
}));
|
|
51
|
-
console.log();
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function printStep(step: number, total: number, label: string): void {
|
|
55
|
-
console.log();
|
|
56
|
-
console.log(accentGradient(` Step ${step} of ${total}`) + pc.dim(` — ${label}`));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function printDone(message: string): void {
|
|
60
|
-
console.log(` ${pc.green('✓')} ${message}`);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function printInfo(message: string): void {
|
|
64
|
-
console.log(` ${pc.dim('│')} ${message}`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function printWarn(message: string): void {
|
|
68
|
-
console.log(` ${pc.yellow('!')} ${message}`);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function printError(message: string): void {
|
|
72
|
-
console.log(` ${pc.red('✗')} ${message}`);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export { pc, brandGradient, accentGradient };
|
|
@@ -1,150 +0,0 @@
|
|
|
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
|
-
});
|