@exagent/agent 0.3.3 → 0.3.4
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-F4TCYYTD.js → chunk-WTECTX2Z.js} +13 -1
- package/dist/cli.js +155 -78
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli.ts +122 -3
- package/src/config.ts +18 -1
- package/src/setup.ts +46 -64
- package/dist/chunk-25J5ZDKX.js +0 -5622
- package/dist/chunk-2ML4XS5X.js +0 -5626
- package/dist/chunk-2OKYNZ3J.js +0 -5622
- package/dist/chunk-2PAASZJN.js +0 -5132
- package/dist/chunk-2WAYVOLU.js +0 -5624
- package/dist/chunk-2XBVVY3I.js +0 -5621
- package/dist/chunk-37HCPVBZ.js +0 -2941
- package/dist/chunk-3DZAZBLF.js +0 -5652
- package/dist/chunk-3IXCKNSV.js +0 -5008
- package/dist/chunk-3MXFTRXB.js +0 -3326
- package/dist/chunk-4MURFLNJ.js +0 -5136
- package/dist/chunk-4UVMO6ZM.js +0 -6318
- package/dist/chunk-56YQROFU.js +0 -5639
- package/dist/chunk-5B3ZEGMD.js +0 -4330
- package/dist/chunk-5NU6FDDE.js +0 -6020
- package/dist/chunk-5WADKPJU.js +0 -1552
- package/dist/chunk-6LOIFEEV.js +0 -4895
- package/dist/chunk-6YIXPNL4.js +0 -5626
- package/dist/chunk-72HK2V74.js +0 -5224
- package/dist/chunk-7Q72QQIV.js +0 -4697
- package/dist/chunk-7V3XTBIF.js +0 -4896
- package/dist/chunk-A4CM4F7Q.js +0 -5633
- package/dist/chunk-ADXXR2MA.js +0 -4816
- package/dist/chunk-AG6CJPIC.js +0 -4895
- package/dist/chunk-AJPXHBTF.js +0 -4459
- package/dist/chunk-AQ6R37XV.js +0 -4809
- package/dist/chunk-AS4UEZMU.js +0 -5001
- package/dist/chunk-ASYMD22Y.js +0 -5624
- package/dist/chunk-AU4MCQCE.js +0 -5624
- package/dist/chunk-B4VHIITU.js +0 -5748
- package/dist/chunk-B6GVDNKQ.js +0 -5624
- package/dist/chunk-BCIAW6ZL.js +0 -5132
- package/dist/chunk-BJZ5PCG3.js +0 -5358
- package/dist/chunk-BS4J5QSM.js +0 -5622
- package/dist/chunk-BV2AUUX6.js +0 -2940
- package/dist/chunk-BWNSH2LK.js +0 -1574
- package/dist/chunk-C3GMBW3R.js +0 -5640
- package/dist/chunk-C6CVQGJ4.js +0 -5624
- package/dist/chunk-CGXKXNUJ.js +0 -5626
- package/dist/chunk-CIEZAYOU.js +0 -4701
- package/dist/chunk-CORZCEAQ.js +0 -5621
- package/dist/chunk-CS5LGZWP.js +0 -5357
- package/dist/chunk-CVT3KC24.js +0 -5624
- package/dist/chunk-D5MJ45R7.js +0 -3258
- package/dist/chunk-DSBRZ5DZ.js +0 -5624
- package/dist/chunk-E2X7JARQ.js +0 -4437
- package/dist/chunk-E3GY36ZP.js +0 -3258
- package/dist/chunk-E6NCIFKB.js +0 -4733
- package/dist/chunk-EOXLKW4D.js +0 -4895
- package/dist/chunk-FCI7LX4Q.js +0 -5624
- package/dist/chunk-FFJSKTOL.js +0 -4539
- package/dist/chunk-FOQYP3IB.js +0 -2950
- package/dist/chunk-GNEYTZDH.js +0 -4686
- package/dist/chunk-GPMXUMYH.js +0 -5991
- package/dist/chunk-GZWPAQPU.js +0 -4593
- package/dist/chunk-H5DXDKMX.js +0 -5619
- package/dist/chunk-HFQRTMS6.js +0 -3377
- package/dist/chunk-HQKRHX6Y.js +0 -5626
- package/dist/chunk-HTF3TNBY.js +0 -4834
- package/dist/chunk-IADSQBBY.js +0 -5523
- package/dist/chunk-IE2SXMZK.js +0 -4890
- package/dist/chunk-IGUQVJCB.js +0 -5622
- package/dist/chunk-IIREL7SL.js +0 -5615
- package/dist/chunk-IJK4EFTJ.js +0 -6043
- package/dist/chunk-J2MQ3Y5O.js +0 -5223
- package/dist/chunk-J3NG7AGT.js +0 -6047
- package/dist/chunk-JIBBZ3NV.js +0 -5132
- package/dist/chunk-JIPSBE6S.js +0 -5622
- package/dist/chunk-JPG755XK.js +0 -4589
- package/dist/chunk-JQBNL5GX.js +0 -5230
- package/dist/chunk-KS3F5WSX.js +0 -4831
- package/dist/chunk-KUYTQ4FR.js +0 -4808
- package/dist/chunk-KVP4CMJ5.js +0 -4711
- package/dist/chunk-LAR2I44B.js +0 -5626
- package/dist/chunk-LBTHSED2.js +0 -1531
- package/dist/chunk-M6OAMYVM.js +0 -5621
- package/dist/chunk-MFN5WWOY.js +0 -5132
- package/dist/chunk-MMTSKXLK.js +0 -5624
- package/dist/chunk-MPUSQLTH.js +0 -5626
- package/dist/chunk-MREXDTWL.js +0 -1555
- package/dist/chunk-MUEDKRFC.js +0 -5624
- package/dist/chunk-NOVPL2JH.js +0 -3327
- package/dist/chunk-NQIP4MHV.js +0 -4334
- package/dist/chunk-NXXKMYLS.js +0 -5624
- package/dist/chunk-OBYNZXNM.js +0 -4756
- package/dist/chunk-OFY4HBOJ.js +0 -5624
- package/dist/chunk-OJNUEZEK.js +0 -5602
- package/dist/chunk-OQCJOMUQ.js +0 -5624
- package/dist/chunk-OZH75GY6.js +0 -5132
- package/dist/chunk-P3IJVDMZ.js +0 -4700
- package/dist/chunk-PMYMYMBH.js +0 -4877
- package/dist/chunk-PRELNRVN.js +0 -5623
- package/dist/chunk-PSQUSNSI.js +0 -4703
- package/dist/chunk-QAIQ5IB6.js +0 -5624
- package/dist/chunk-QG22GADV.js +0 -6316
- package/dist/chunk-QNE2KGGK.js +0 -3315
- package/dist/chunk-RH7ZBSG4.js +0 -5132
- package/dist/chunk-RLD5MUCR.js +0 -5626
- package/dist/chunk-S42VEBNR.js +0 -3268
- package/dist/chunk-SEM6UXU4.js +0 -3324
- package/dist/chunk-SI5WP77M.js +0 -4430
- package/dist/chunk-SID4SQSY.js +0 -4837
- package/dist/chunk-SIELPKWF.js +0 -1558
- package/dist/chunk-SVBLY6QT.js +0 -5742
- package/dist/chunk-SVFTC5V2.js +0 -6021
- package/dist/chunk-SXHTX62B.js +0 -4823
- package/dist/chunk-T2YCEA5U.js +0 -4730
- package/dist/chunk-TARCHIOU.js +0 -4718
- package/dist/chunk-TDACLKD7.js +0 -5867
- package/dist/chunk-TGCBM3NP.js +0 -4890
- package/dist/chunk-TIWG6KAK.js +0 -4769
- package/dist/chunk-TKLKATVM.js +0 -1534
- package/dist/chunk-TSLZ4A5P.js +0 -5222
- package/dist/chunk-TWSDKORW.js +0 -4698
- package/dist/chunk-U5QHYVMJ.js +0 -3341
- package/dist/chunk-UAP5CTHB.js +0 -5985
- package/dist/chunk-UK6SEUWU.js +0 -3210
- package/dist/chunk-UKU5YO65.js +0 -5132
- package/dist/chunk-UOZQXP4Q.js +0 -5144
- package/dist/chunk-UPTN2TSS.js +0 -4727
- package/dist/chunk-UQT2APOE.js +0 -2944
- package/dist/chunk-V32QDZKW.js +0 -5132
- package/dist/chunk-VDK4XPAC.js +0 -6318
- package/dist/chunk-VKY2CDCD.js +0 -5622
- package/dist/chunk-VUCSYMCY.js +0 -3323
- package/dist/chunk-VVLNBD5Y.js +0 -5132
- package/dist/chunk-W3TQ22O6.js +0 -4459
- package/dist/chunk-WA4DSGOM.js +0 -3355
- package/dist/chunk-WI6MIICK.js +0 -4687
- package/dist/chunk-XHYHRJMK.js +0 -6319
- package/dist/chunk-XRHJLL74.js +0 -4893
- package/dist/chunk-XXWXEBJQ.js +0 -4885
- package/dist/chunk-YC6TH2H3.js +0 -5624
- package/dist/chunk-YDH6HCUJ.js +0 -5624
- package/dist/chunk-YJD35VKQ.js +0 -4890
- package/dist/chunk-YTZ5MZLP.js +0 -6318
- package/dist/chunk-ZBIQJBY7.js +0 -5620
- package/dist/chunk-ZKTSA2AE.js +0 -5629
- package/dist/chunk-ZKZZL3PE.js +0 -3379
- package/dist/chunk-ZM5KCPRK.js +0 -4541
- package/dist/chunk-ZTYPDSE3.js +0 -3258
package/dist/chunk-MREXDTWL.js
DELETED
|
@@ -1,1555 +0,0 @@
|
|
|
1
|
-
// src/config.ts
|
|
2
|
-
import { readFileSync, existsSync, writeFileSync } from "fs";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
var configSchema = z.object({
|
|
5
|
-
agentId: z.string(),
|
|
6
|
-
apiUrl: z.string().url(),
|
|
7
|
-
apiToken: z.string(),
|
|
8
|
-
wallet: z.object({
|
|
9
|
-
privateKey: z.string()
|
|
10
|
-
}).optional(),
|
|
11
|
-
llm: z.object({
|
|
12
|
-
provider: z.enum(["openai", "anthropic", "google", "deepseek", "mistral", "groq", "together", "ollama"]),
|
|
13
|
-
model: z.string().optional(),
|
|
14
|
-
apiKey: z.string().optional(),
|
|
15
|
-
endpoint: z.string().optional(),
|
|
16
|
-
temperature: z.number().min(0).max(2).optional(),
|
|
17
|
-
maxTokens: z.number().optional()
|
|
18
|
-
}),
|
|
19
|
-
strategy: z.object({
|
|
20
|
-
file: z.string().optional(),
|
|
21
|
-
template: z.string().optional()
|
|
22
|
-
}),
|
|
23
|
-
trading: z.object({
|
|
24
|
-
mode: z.enum(["live", "paper"]).default("paper"),
|
|
25
|
-
timeHorizon: z.enum(["intraday", "swing", "position"]).default("swing"),
|
|
26
|
-
maxPositionSizeBps: z.number().min(100).max(1e4).default(2e3),
|
|
27
|
-
maxDailyLossBps: z.number().min(0).max(1e4).default(500),
|
|
28
|
-
maxConcurrentPositions: z.number().min(1).max(100).default(5),
|
|
29
|
-
tradingIntervalMs: z.number().min(1e3).default(6e4),
|
|
30
|
-
maxSlippageBps: z.number().min(10).max(1e3).default(100),
|
|
31
|
-
minTradeValueUSD: z.number().min(0).default(10),
|
|
32
|
-
initialCapitalUSD: z.number().optional()
|
|
33
|
-
}),
|
|
34
|
-
venues: z.object({
|
|
35
|
-
hyperliquid_perp: z.object({
|
|
36
|
-
enabled: z.boolean().default(false),
|
|
37
|
-
apiUrl: z.string().default("https://api.hyperliquid.xyz"),
|
|
38
|
-
wsUrl: z.string().default("wss://api.hyperliquid.xyz/ws"),
|
|
39
|
-
maxLeverage: z.number().min(1).max(50).default(10),
|
|
40
|
-
maxNotionalUSD: z.number().default(5e4),
|
|
41
|
-
allowedInstruments: z.array(z.string()).optional()
|
|
42
|
-
}).optional(),
|
|
43
|
-
polymarket: z.object({
|
|
44
|
-
enabled: z.boolean().default(false),
|
|
45
|
-
clobApiUrl: z.string().default("https://clob.polymarket.com"),
|
|
46
|
-
gammaApiUrl: z.string().default("https://gamma-api.polymarket.com"),
|
|
47
|
-
maxNotionalUSD: z.number().default(1e3),
|
|
48
|
-
maxTotalExposureUSD: z.number().default(5e3),
|
|
49
|
-
allowedCategories: z.array(z.string()).optional()
|
|
50
|
-
}).optional()
|
|
51
|
-
}).optional(),
|
|
52
|
-
relay: z.object({
|
|
53
|
-
url: z.string(),
|
|
54
|
-
heartbeatIntervalMs: z.number().default(3e4),
|
|
55
|
-
reconnectMaxAttempts: z.number().default(50)
|
|
56
|
-
})
|
|
57
|
-
});
|
|
58
|
-
function loadConfig(path = "agent-config.json") {
|
|
59
|
-
if (!existsSync(path)) {
|
|
60
|
-
throw new Error(`Config file not found: ${path}. Run 'exagent init' first.`);
|
|
61
|
-
}
|
|
62
|
-
const raw = readFileSync(path, "utf-8");
|
|
63
|
-
let parsed;
|
|
64
|
-
try {
|
|
65
|
-
parsed = JSON.parse(raw);
|
|
66
|
-
} catch {
|
|
67
|
-
throw new Error(`Invalid JSON in ${path}`);
|
|
68
|
-
}
|
|
69
|
-
const config = parsed;
|
|
70
|
-
const llm = config.llm || {};
|
|
71
|
-
if (process.env.EXAGENT_LLM_PROVIDER) llm.provider = process.env.EXAGENT_LLM_PROVIDER;
|
|
72
|
-
if (process.env.EXAGENT_LLM_MODEL) llm.model = process.env.EXAGENT_LLM_MODEL;
|
|
73
|
-
if (process.env.EXAGENT_LLM_API_KEY) llm.apiKey = process.env.EXAGENT_LLM_API_KEY;
|
|
74
|
-
if (process.env.EXAGENT_API_URL) config.apiUrl = process.env.EXAGENT_API_URL;
|
|
75
|
-
if (process.env.EXAGENT_API_TOKEN) config.apiToken = process.env.EXAGENT_API_TOKEN;
|
|
76
|
-
if (process.env.EXAGENT_WALLET_PRIVATE_KEY) {
|
|
77
|
-
config.wallet = { privateKey: process.env.EXAGENT_WALLET_PRIVATE_KEY };
|
|
78
|
-
}
|
|
79
|
-
config.llm = llm;
|
|
80
|
-
const result = configSchema.safeParse(config);
|
|
81
|
-
if (!result.success) {
|
|
82
|
-
const issues = result.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
83
|
-
throw new Error(`Invalid config:
|
|
84
|
-
${issues}`);
|
|
85
|
-
}
|
|
86
|
-
return result.data;
|
|
87
|
-
}
|
|
88
|
-
function generateSampleConfig(agentId, apiUrl) {
|
|
89
|
-
const config = {
|
|
90
|
-
agentId,
|
|
91
|
-
apiUrl,
|
|
92
|
-
apiToken: "<your-jwt-token>",
|
|
93
|
-
llm: {
|
|
94
|
-
provider: "openai",
|
|
95
|
-
model: "gpt-4o",
|
|
96
|
-
apiKey: "<your-api-key>"
|
|
97
|
-
},
|
|
98
|
-
strategy: {
|
|
99
|
-
template: "momentum"
|
|
100
|
-
},
|
|
101
|
-
trading: {
|
|
102
|
-
mode: "paper",
|
|
103
|
-
timeHorizon: "swing",
|
|
104
|
-
maxPositionSizeBps: 2e3,
|
|
105
|
-
maxDailyLossBps: 500,
|
|
106
|
-
maxConcurrentPositions: 5,
|
|
107
|
-
tradingIntervalMs: 6e4,
|
|
108
|
-
maxSlippageBps: 100,
|
|
109
|
-
minTradeValueUSD: 10,
|
|
110
|
-
initialCapitalUSD: 1e4
|
|
111
|
-
},
|
|
112
|
-
relay: {
|
|
113
|
-
url: apiUrl.replace(/^http/, "ws") + "/ws/agent",
|
|
114
|
-
heartbeatIntervalMs: 3e4,
|
|
115
|
-
reconnectMaxAttempts: 50
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
return JSON.stringify(config, null, 2);
|
|
119
|
-
}
|
|
120
|
-
function writeSampleConfig(agentId, apiUrl, path = "agent-config.json") {
|
|
121
|
-
writeFileSync(path, generateSampleConfig(agentId, apiUrl));
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// src/relay.ts
|
|
125
|
-
import WebSocket from "ws";
|
|
126
|
-
var RelayClient = class {
|
|
127
|
-
ws = null;
|
|
128
|
-
config;
|
|
129
|
-
heartbeatTimer = null;
|
|
130
|
-
reconnectTimer = null;
|
|
131
|
-
reconnectAttempts = 0;
|
|
132
|
-
intentionalClose = false;
|
|
133
|
-
authenticated = false;
|
|
134
|
-
lastStatus = null;
|
|
135
|
-
onCommand;
|
|
136
|
-
onConnected;
|
|
137
|
-
onDisconnected;
|
|
138
|
-
constructor(config) {
|
|
139
|
-
this.config = {
|
|
140
|
-
url: config.url,
|
|
141
|
-
agentId: config.agentId,
|
|
142
|
-
token: config.token,
|
|
143
|
-
heartbeatIntervalMs: config.heartbeatIntervalMs ?? 3e4,
|
|
144
|
-
reconnectMaxAttempts: config.reconnectMaxAttempts ?? 50
|
|
145
|
-
};
|
|
146
|
-
this.onCommand = config.onCommand;
|
|
147
|
-
this.onConnected = config.onConnected;
|
|
148
|
-
this.onDisconnected = config.onDisconnected;
|
|
149
|
-
}
|
|
150
|
-
get isConnected() {
|
|
151
|
-
return this.ws?.readyState === WebSocket.OPEN && this.authenticated;
|
|
152
|
-
}
|
|
153
|
-
async connect() {
|
|
154
|
-
return new Promise((resolve2, reject) => {
|
|
155
|
-
try {
|
|
156
|
-
this.intentionalClose = false;
|
|
157
|
-
this.ws = new WebSocket(this.config.url);
|
|
158
|
-
const timeout = setTimeout(() => {
|
|
159
|
-
if (this.ws && this.ws.readyState !== WebSocket.OPEN) {
|
|
160
|
-
this.ws.terminate();
|
|
161
|
-
reject(new Error("Connection timeout (15s)"));
|
|
162
|
-
}
|
|
163
|
-
}, 15e3);
|
|
164
|
-
this.ws.on("open", () => {
|
|
165
|
-
clearTimeout(timeout);
|
|
166
|
-
this.reconnectAttempts = 0;
|
|
167
|
-
this.authenticate();
|
|
168
|
-
});
|
|
169
|
-
this.ws.on("message", (data) => {
|
|
170
|
-
try {
|
|
171
|
-
const msg = JSON.parse(data.toString());
|
|
172
|
-
this.handleMessage(msg, resolve2);
|
|
173
|
-
} catch {
|
|
174
|
-
console.error("[relay] Failed to parse message");
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
this.ws.on("close", () => {
|
|
178
|
-
this.cleanup();
|
|
179
|
-
this.onDisconnected?.();
|
|
180
|
-
if (!this.intentionalClose) {
|
|
181
|
-
this.scheduleReconnect();
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
this.ws.on("error", (err) => {
|
|
185
|
-
console.error("[relay] WebSocket error:", err.message);
|
|
186
|
-
if (!this.authenticated) {
|
|
187
|
-
clearTimeout(timeout);
|
|
188
|
-
reject(err);
|
|
189
|
-
}
|
|
190
|
-
});
|
|
191
|
-
} catch (err) {
|
|
192
|
-
reject(err);
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
disconnect() {
|
|
197
|
-
this.intentionalClose = true;
|
|
198
|
-
this.cleanup();
|
|
199
|
-
if (this.ws) {
|
|
200
|
-
this.ws.close();
|
|
201
|
-
this.ws = null;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
authenticate() {
|
|
205
|
-
this.send({
|
|
206
|
-
type: "auth",
|
|
207
|
-
agentId: this.config.agentId,
|
|
208
|
-
token: this.config.token
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
handleMessage(msg, onAuth) {
|
|
212
|
-
switch (msg.type) {
|
|
213
|
-
case "auth_success":
|
|
214
|
-
this.authenticated = true;
|
|
215
|
-
this.startHeartbeat();
|
|
216
|
-
this.onConnected?.();
|
|
217
|
-
onAuth?.();
|
|
218
|
-
console.log(`[relay] Authenticated as agent ${this.config.agentId}`);
|
|
219
|
-
break;
|
|
220
|
-
case "auth_error":
|
|
221
|
-
console.error("[relay] Auth failed:", msg.error);
|
|
222
|
-
this.disconnect();
|
|
223
|
-
break;
|
|
224
|
-
case "command":
|
|
225
|
-
this.onCommand?.(msg.command);
|
|
226
|
-
break;
|
|
227
|
-
case "pong":
|
|
228
|
-
break;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
send(msg) {
|
|
232
|
-
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
233
|
-
this.ws.send(JSON.stringify(msg));
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
sendHeartbeat(status) {
|
|
237
|
-
this.lastStatus = status;
|
|
238
|
-
this.send({
|
|
239
|
-
type: "heartbeat",
|
|
240
|
-
agentId: this.config.agentId,
|
|
241
|
-
status,
|
|
242
|
-
timestamp: Date.now()
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
sendMessage(messageType, level, title, body, data) {
|
|
246
|
-
this.send({
|
|
247
|
-
type: "message",
|
|
248
|
-
agentId: this.config.agentId,
|
|
249
|
-
messageType,
|
|
250
|
-
level,
|
|
251
|
-
title,
|
|
252
|
-
body,
|
|
253
|
-
data,
|
|
254
|
-
timestamp: Date.now()
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
sendTradeSignal(signal) {
|
|
258
|
-
this.send({
|
|
259
|
-
type: "trade_signal",
|
|
260
|
-
agentId: this.config.agentId,
|
|
261
|
-
signal,
|
|
262
|
-
timestamp: Date.now()
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
startHeartbeat() {
|
|
266
|
-
this.stopHeartbeat();
|
|
267
|
-
this.heartbeatTimer = setInterval(() => {
|
|
268
|
-
if (this.lastStatus) {
|
|
269
|
-
this.sendHeartbeat(this.lastStatus);
|
|
270
|
-
}
|
|
271
|
-
}, this.config.heartbeatIntervalMs);
|
|
272
|
-
}
|
|
273
|
-
stopHeartbeat() {
|
|
274
|
-
if (this.heartbeatTimer) {
|
|
275
|
-
clearInterval(this.heartbeatTimer);
|
|
276
|
-
this.heartbeatTimer = null;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
cleanup() {
|
|
280
|
-
this.stopHeartbeat();
|
|
281
|
-
this.authenticated = false;
|
|
282
|
-
if (this.reconnectTimer) {
|
|
283
|
-
clearTimeout(this.reconnectTimer);
|
|
284
|
-
this.reconnectTimer = null;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
scheduleReconnect() {
|
|
288
|
-
if (this.reconnectAttempts >= this.config.reconnectMaxAttempts) {
|
|
289
|
-
console.error(`[relay] Max reconnect attempts (${this.config.reconnectMaxAttempts}) reached`);
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
|
|
293
|
-
this.reconnectAttempts++;
|
|
294
|
-
console.log(`[relay] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
295
|
-
this.reconnectTimer = setTimeout(async () => {
|
|
296
|
-
try {
|
|
297
|
-
await this.connect();
|
|
298
|
-
} catch (err) {
|
|
299
|
-
console.error("[relay] Reconnect failed:", err.message);
|
|
300
|
-
}
|
|
301
|
-
}, delay);
|
|
302
|
-
}
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
// src/signal.ts
|
|
306
|
-
var SignalReporter = class {
|
|
307
|
-
relay;
|
|
308
|
-
constructor(relay) {
|
|
309
|
-
this.relay = relay;
|
|
310
|
-
}
|
|
311
|
-
reportTrade(signal) {
|
|
312
|
-
if (!this.relay.isConnected) {
|
|
313
|
-
console.warn("[signal] Not connected to relay \u2014 trade signal queued locally");
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
this.relay.sendTradeSignal(signal);
|
|
317
|
-
this.relay.sendMessage(
|
|
318
|
-
"trade_executed",
|
|
319
|
-
"success",
|
|
320
|
-
`${signal.side.toUpperCase()} ${signal.symbol}`,
|
|
321
|
-
`${signal.side} ${signal.size} ${signal.symbol} @ $${signal.price.toFixed(2)} on ${signal.venue}`,
|
|
322
|
-
{ signal }
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
|
-
reportPerpFill(signal) {
|
|
326
|
-
if (!this.relay.isConnected) return;
|
|
327
|
-
this.relay.sendTradeSignal(signal);
|
|
328
|
-
this.relay.sendMessage(
|
|
329
|
-
"perp_fill",
|
|
330
|
-
"success",
|
|
331
|
-
`Perp ${signal.side.toUpperCase()} ${signal.symbol}`,
|
|
332
|
-
`${signal.side} ${signal.size} ${signal.symbol} @ $${signal.price.toFixed(2)} (${signal.leverage ?? 1}x)`,
|
|
333
|
-
{ signal }
|
|
334
|
-
);
|
|
335
|
-
}
|
|
336
|
-
reportPredictionFill(signal) {
|
|
337
|
-
if (!this.relay.isConnected) return;
|
|
338
|
-
this.relay.sendTradeSignal(signal);
|
|
339
|
-
this.relay.sendMessage(
|
|
340
|
-
"prediction_fill",
|
|
341
|
-
"success",
|
|
342
|
-
`Prediction ${signal.side.toUpperCase()}`,
|
|
343
|
-
`${signal.side} $${signal.size.toFixed(2)} on ${signal.symbol} @ ${signal.price.toFixed(4)}`,
|
|
344
|
-
{ signal }
|
|
345
|
-
);
|
|
346
|
-
}
|
|
347
|
-
reportError(title, body, data) {
|
|
348
|
-
if (!this.relay.isConnected) return;
|
|
349
|
-
this.relay.sendMessage("error", "error", title, body, data);
|
|
350
|
-
}
|
|
351
|
-
reportInfo(title, body, data) {
|
|
352
|
-
if (!this.relay.isConnected) return;
|
|
353
|
-
this.relay.sendMessage("info", "info", title, body, data);
|
|
354
|
-
}
|
|
355
|
-
};
|
|
356
|
-
|
|
357
|
-
// src/store.ts
|
|
358
|
-
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync, existsSync as existsSync2 } from "fs";
|
|
359
|
-
import { dirname } from "path";
|
|
360
|
-
var FileStore = class {
|
|
361
|
-
data = {};
|
|
362
|
-
filePath;
|
|
363
|
-
constructor(filePath = "data/strategy-store.json") {
|
|
364
|
-
this.filePath = filePath;
|
|
365
|
-
this.load();
|
|
366
|
-
}
|
|
367
|
-
load() {
|
|
368
|
-
try {
|
|
369
|
-
if (existsSync2(this.filePath)) {
|
|
370
|
-
const raw = readFileSync2(this.filePath, "utf-8");
|
|
371
|
-
this.data = JSON.parse(raw);
|
|
372
|
-
}
|
|
373
|
-
} catch {
|
|
374
|
-
this.data = {};
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
flush() {
|
|
378
|
-
const dir = dirname(this.filePath);
|
|
379
|
-
if (!existsSync2(dir)) {
|
|
380
|
-
mkdirSync(dir, { recursive: true });
|
|
381
|
-
}
|
|
382
|
-
writeFileSync2(this.filePath, JSON.stringify(this.data, null, 2));
|
|
383
|
-
}
|
|
384
|
-
get(key) {
|
|
385
|
-
return this.data[key];
|
|
386
|
-
}
|
|
387
|
-
set(key, value) {
|
|
388
|
-
this.data[key] = value;
|
|
389
|
-
this.flush();
|
|
390
|
-
}
|
|
391
|
-
delete(key) {
|
|
392
|
-
delete this.data[key];
|
|
393
|
-
this.flush();
|
|
394
|
-
}
|
|
395
|
-
keys() {
|
|
396
|
-
return Object.keys(this.data);
|
|
397
|
-
}
|
|
398
|
-
};
|
|
399
|
-
|
|
400
|
-
// src/position-tracker.ts
|
|
401
|
-
var BASE_ASSETS = /* @__PURE__ */ new Set(["ETH", "WETH", "USDC", "USDbC", "DAI", "USDT", "EURC"]);
|
|
402
|
-
var PositionTracker = class {
|
|
403
|
-
positions = /* @__PURE__ */ new Map();
|
|
404
|
-
trades = [];
|
|
405
|
-
realizedPnL = 0;
|
|
406
|
-
store;
|
|
407
|
-
constructor(store) {
|
|
408
|
-
this.store = store;
|
|
409
|
-
this.load();
|
|
410
|
-
}
|
|
411
|
-
load() {
|
|
412
|
-
const saved = this.store.get("position_tracker");
|
|
413
|
-
if (saved) {
|
|
414
|
-
this.positions = new Map(saved.positions);
|
|
415
|
-
this.trades = saved.trades;
|
|
416
|
-
this.realizedPnL = saved.realizedPnL;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
save() {
|
|
420
|
-
this.store.set("position_tracker", {
|
|
421
|
-
positions: Array.from(this.positions.entries()),
|
|
422
|
-
trades: this.trades.slice(-1e3),
|
|
423
|
-
realizedPnL: this.realizedPnL
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
recordBuy(token, quantity, price, fee, venue, chain, venueFillId) {
|
|
427
|
-
if (BASE_ASSETS.has(token)) return;
|
|
428
|
-
const existing = this.positions.get(token);
|
|
429
|
-
if (existing) {
|
|
430
|
-
const totalQty = existing.quantity + quantity;
|
|
431
|
-
const totalCost = existing.costBasisPerUnit * existing.quantity + price * quantity;
|
|
432
|
-
existing.costBasisPerUnit = totalCost / totalQty;
|
|
433
|
-
existing.quantity = totalQty;
|
|
434
|
-
} else {
|
|
435
|
-
this.positions.set(token, {
|
|
436
|
-
token,
|
|
437
|
-
quantity,
|
|
438
|
-
costBasisPerUnit: price,
|
|
439
|
-
entryTimestamp: Date.now(),
|
|
440
|
-
venue,
|
|
441
|
-
chain
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
this.trades.push({
|
|
445
|
-
token,
|
|
446
|
-
action: "buy",
|
|
447
|
-
quantity,
|
|
448
|
-
price,
|
|
449
|
-
fee,
|
|
450
|
-
timestamp: Date.now(),
|
|
451
|
-
venue,
|
|
452
|
-
chain,
|
|
453
|
-
venueFillId
|
|
454
|
-
});
|
|
455
|
-
this.save();
|
|
456
|
-
}
|
|
457
|
-
recordSell(token, quantity, price, fee, venue, chain, venueFillId) {
|
|
458
|
-
if (BASE_ASSETS.has(token)) return;
|
|
459
|
-
const existing = this.positions.get(token);
|
|
460
|
-
if (existing) {
|
|
461
|
-
const pnl = (price - existing.costBasisPerUnit) * quantity - fee;
|
|
462
|
-
this.realizedPnL += pnl;
|
|
463
|
-
existing.quantity -= quantity;
|
|
464
|
-
if (existing.quantity <= 1e-6) {
|
|
465
|
-
this.positions.delete(token);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
this.trades.push({
|
|
469
|
-
token,
|
|
470
|
-
action: "sell",
|
|
471
|
-
quantity,
|
|
472
|
-
price,
|
|
473
|
-
fee,
|
|
474
|
-
timestamp: Date.now(),
|
|
475
|
-
venue,
|
|
476
|
-
chain,
|
|
477
|
-
venueFillId
|
|
478
|
-
});
|
|
479
|
-
this.save();
|
|
480
|
-
}
|
|
481
|
-
getSummary(prices) {
|
|
482
|
-
let totalUnrealizedPnL = 0;
|
|
483
|
-
const openPositions = Array.from(this.positions.values());
|
|
484
|
-
for (const pos of openPositions) {
|
|
485
|
-
const currentPrice = prices[pos.token];
|
|
486
|
-
if (currentPrice) {
|
|
487
|
-
totalUnrealizedPnL += (currentPrice - pos.costBasisPerUnit) * pos.quantity;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
return {
|
|
491
|
-
openPositions,
|
|
492
|
-
totalUnrealizedPnL,
|
|
493
|
-
totalRealizedPnL: this.realizedPnL
|
|
494
|
-
};
|
|
495
|
-
}
|
|
496
|
-
getPositions() {
|
|
497
|
-
return Array.from(this.positions.values());
|
|
498
|
-
}
|
|
499
|
-
getTrades(limit) {
|
|
500
|
-
if (limit) return this.trades.slice(-limit);
|
|
501
|
-
return [...this.trades];
|
|
502
|
-
}
|
|
503
|
-
getRealizedPnL() {
|
|
504
|
-
return this.realizedPnL;
|
|
505
|
-
}
|
|
506
|
-
reset() {
|
|
507
|
-
this.positions.clear();
|
|
508
|
-
this.trades = [];
|
|
509
|
-
this.realizedPnL = 0;
|
|
510
|
-
this.save();
|
|
511
|
-
}
|
|
512
|
-
};
|
|
513
|
-
|
|
514
|
-
// src/llm/base.ts
|
|
515
|
-
var BaseLLMAdapter = class {
|
|
516
|
-
config;
|
|
517
|
-
constructor(config) {
|
|
518
|
-
this.config = config;
|
|
519
|
-
}
|
|
520
|
-
getTemperature() {
|
|
521
|
-
return this.config.temperature ?? 0.7;
|
|
522
|
-
}
|
|
523
|
-
getMaxTokens() {
|
|
524
|
-
return this.config.maxTokens ?? 4096;
|
|
525
|
-
}
|
|
526
|
-
};
|
|
527
|
-
|
|
528
|
-
// src/llm/openai.ts
|
|
529
|
-
var OpenAIAdapter = class extends BaseLLMAdapter {
|
|
530
|
-
endpoint;
|
|
531
|
-
constructor(config) {
|
|
532
|
-
super(config);
|
|
533
|
-
this.endpoint = config.endpoint || "https://api.openai.com/v1";
|
|
534
|
-
}
|
|
535
|
-
async chat(messages) {
|
|
536
|
-
const res = await fetch(`${this.endpoint}/chat/completions`, {
|
|
537
|
-
method: "POST",
|
|
538
|
-
headers: {
|
|
539
|
-
"Content-Type": "application/json",
|
|
540
|
-
Authorization: `Bearer ${this.config.apiKey}`
|
|
541
|
-
},
|
|
542
|
-
body: JSON.stringify({
|
|
543
|
-
model: this.config.model || "gpt-4o",
|
|
544
|
-
messages: messages.map((m) => ({ role: m.role, content: m.content })),
|
|
545
|
-
temperature: this.getTemperature(),
|
|
546
|
-
max_tokens: this.getMaxTokens()
|
|
547
|
-
})
|
|
548
|
-
});
|
|
549
|
-
if (!res.ok) {
|
|
550
|
-
const body = await res.text();
|
|
551
|
-
throw new Error(`OpenAI API error ${res.status}: ${body}`);
|
|
552
|
-
}
|
|
553
|
-
const data = await res.json();
|
|
554
|
-
return {
|
|
555
|
-
content: data.choices[0]?.message?.content || "",
|
|
556
|
-
tokens: data.usage ? { input: data.usage.prompt_tokens, output: data.usage.completion_tokens } : void 0
|
|
557
|
-
};
|
|
558
|
-
}
|
|
559
|
-
getMetadata() {
|
|
560
|
-
return {
|
|
561
|
-
provider: "openai",
|
|
562
|
-
model: this.config.model || "gpt-4o"
|
|
563
|
-
};
|
|
564
|
-
}
|
|
565
|
-
};
|
|
566
|
-
|
|
567
|
-
// src/llm/anthropic.ts
|
|
568
|
-
var AnthropicAdapter = class extends BaseLLMAdapter {
|
|
569
|
-
endpoint;
|
|
570
|
-
constructor(config) {
|
|
571
|
-
super(config);
|
|
572
|
-
this.endpoint = config.endpoint || "https://api.anthropic.com";
|
|
573
|
-
}
|
|
574
|
-
async chat(messages) {
|
|
575
|
-
const systemMessage = messages.find((m) => m.role === "system");
|
|
576
|
-
const nonSystemMessages = messages.filter((m) => m.role !== "system");
|
|
577
|
-
const body = {
|
|
578
|
-
model: this.config.model || "claude-sonnet-4-20250514",
|
|
579
|
-
messages: nonSystemMessages.map((m) => ({ role: m.role, content: m.content })),
|
|
580
|
-
max_tokens: this.getMaxTokens(),
|
|
581
|
-
temperature: this.getTemperature()
|
|
582
|
-
};
|
|
583
|
-
if (systemMessage) {
|
|
584
|
-
body.system = systemMessage.content;
|
|
585
|
-
}
|
|
586
|
-
const res = await fetch(`${this.endpoint}/v1/messages`, {
|
|
587
|
-
method: "POST",
|
|
588
|
-
headers: {
|
|
589
|
-
"Content-Type": "application/json",
|
|
590
|
-
"x-api-key": this.config.apiKey || "",
|
|
591
|
-
"anthropic-version": "2023-06-01"
|
|
592
|
-
},
|
|
593
|
-
body: JSON.stringify(body)
|
|
594
|
-
});
|
|
595
|
-
if (!res.ok) {
|
|
596
|
-
const text = await res.text();
|
|
597
|
-
throw new Error(`Anthropic API error ${res.status}: ${text}`);
|
|
598
|
-
}
|
|
599
|
-
const data = await res.json();
|
|
600
|
-
const textContent = data.content.find((c) => c.type === "text");
|
|
601
|
-
return {
|
|
602
|
-
content: textContent?.text || "",
|
|
603
|
-
tokens: data.usage ? { input: data.usage.input_tokens, output: data.usage.output_tokens } : void 0
|
|
604
|
-
};
|
|
605
|
-
}
|
|
606
|
-
getMetadata() {
|
|
607
|
-
return {
|
|
608
|
-
provider: "anthropic",
|
|
609
|
-
model: this.config.model || "claude-sonnet-4-20250514"
|
|
610
|
-
};
|
|
611
|
-
}
|
|
612
|
-
};
|
|
613
|
-
|
|
614
|
-
// src/llm/google.ts
|
|
615
|
-
var GoogleAdapter = class extends BaseLLMAdapter {
|
|
616
|
-
constructor(config) {
|
|
617
|
-
super(config);
|
|
618
|
-
}
|
|
619
|
-
async chat(messages) {
|
|
620
|
-
const model = this.config.model || "gemini-2.5-flash";
|
|
621
|
-
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${this.config.apiKey}`;
|
|
622
|
-
const systemMessage = messages.find((m) => m.role === "system");
|
|
623
|
-
const nonSystemMessages = messages.filter((m) => m.role !== "system");
|
|
624
|
-
const body = {
|
|
625
|
-
contents: nonSystemMessages.map((m) => ({
|
|
626
|
-
role: m.role === "assistant" ? "model" : "user",
|
|
627
|
-
parts: [{ text: m.content }]
|
|
628
|
-
})),
|
|
629
|
-
generationConfig: {
|
|
630
|
-
temperature: this.getTemperature(),
|
|
631
|
-
maxOutputTokens: this.getMaxTokens()
|
|
632
|
-
}
|
|
633
|
-
};
|
|
634
|
-
if (systemMessage) {
|
|
635
|
-
body.systemInstruction = { parts: [{ text: systemMessage.content }] };
|
|
636
|
-
}
|
|
637
|
-
const res = await fetch(url, {
|
|
638
|
-
method: "POST",
|
|
639
|
-
headers: { "Content-Type": "application/json" },
|
|
640
|
-
body: JSON.stringify(body)
|
|
641
|
-
});
|
|
642
|
-
if (!res.ok) {
|
|
643
|
-
const text2 = await res.text();
|
|
644
|
-
throw new Error(`Google AI error ${res.status}: ${text2}`);
|
|
645
|
-
}
|
|
646
|
-
const data = await res.json();
|
|
647
|
-
const text = data.candidates[0]?.content?.parts[0]?.text || "";
|
|
648
|
-
return {
|
|
649
|
-
content: text,
|
|
650
|
-
tokens: data.usageMetadata ? { input: data.usageMetadata.promptTokenCount, output: data.usageMetadata.candidatesTokenCount } : void 0
|
|
651
|
-
};
|
|
652
|
-
}
|
|
653
|
-
getMetadata() {
|
|
654
|
-
return {
|
|
655
|
-
provider: "google",
|
|
656
|
-
model: this.config.model || "gemini-2.5-flash"
|
|
657
|
-
};
|
|
658
|
-
}
|
|
659
|
-
};
|
|
660
|
-
|
|
661
|
-
// src/llm/deepseek.ts
|
|
662
|
-
var DeepSeekAdapter = class extends BaseLLMAdapter {
|
|
663
|
-
constructor(config) {
|
|
664
|
-
super(config);
|
|
665
|
-
}
|
|
666
|
-
async chat(messages) {
|
|
667
|
-
const res = await fetch("https://api.deepseek.com/chat/completions", {
|
|
668
|
-
method: "POST",
|
|
669
|
-
headers: {
|
|
670
|
-
"Content-Type": "application/json",
|
|
671
|
-
Authorization: `Bearer ${this.config.apiKey}`
|
|
672
|
-
},
|
|
673
|
-
body: JSON.stringify({
|
|
674
|
-
model: this.config.model || "deepseek-chat",
|
|
675
|
-
messages: messages.map((m) => ({ role: m.role, content: m.content })),
|
|
676
|
-
temperature: this.getTemperature(),
|
|
677
|
-
max_tokens: this.getMaxTokens()
|
|
678
|
-
})
|
|
679
|
-
});
|
|
680
|
-
if (!res.ok) {
|
|
681
|
-
const body = await res.text();
|
|
682
|
-
throw new Error(`DeepSeek API error ${res.status}: ${body}`);
|
|
683
|
-
}
|
|
684
|
-
const data = await res.json();
|
|
685
|
-
return {
|
|
686
|
-
content: data.choices[0]?.message?.content || "",
|
|
687
|
-
tokens: data.usage ? { input: data.usage.prompt_tokens, output: data.usage.completion_tokens } : void 0
|
|
688
|
-
};
|
|
689
|
-
}
|
|
690
|
-
getMetadata() {
|
|
691
|
-
return {
|
|
692
|
-
provider: "deepseek",
|
|
693
|
-
model: this.config.model || "deepseek-chat"
|
|
694
|
-
};
|
|
695
|
-
}
|
|
696
|
-
};
|
|
697
|
-
|
|
698
|
-
// src/llm/mistral.ts
|
|
699
|
-
var MistralAdapter = class extends BaseLLMAdapter {
|
|
700
|
-
constructor(config) {
|
|
701
|
-
super(config);
|
|
702
|
-
}
|
|
703
|
-
async chat(messages) {
|
|
704
|
-
const res = await fetch("https://api.mistral.ai/v1/chat/completions", {
|
|
705
|
-
method: "POST",
|
|
706
|
-
headers: {
|
|
707
|
-
"Content-Type": "application/json",
|
|
708
|
-
Authorization: `Bearer ${this.config.apiKey}`
|
|
709
|
-
},
|
|
710
|
-
body: JSON.stringify({
|
|
711
|
-
model: this.config.model || "mistral-large-latest",
|
|
712
|
-
messages: messages.map((m) => ({ role: m.role, content: m.content })),
|
|
713
|
-
temperature: this.getTemperature(),
|
|
714
|
-
max_tokens: this.getMaxTokens()
|
|
715
|
-
})
|
|
716
|
-
});
|
|
717
|
-
if (!res.ok) {
|
|
718
|
-
const body = await res.text();
|
|
719
|
-
throw new Error(`Mistral API error ${res.status}: ${body}`);
|
|
720
|
-
}
|
|
721
|
-
const data = await res.json();
|
|
722
|
-
return {
|
|
723
|
-
content: data.choices[0]?.message?.content || "",
|
|
724
|
-
tokens: data.usage ? { input: data.usage.prompt_tokens, output: data.usage.completion_tokens } : void 0
|
|
725
|
-
};
|
|
726
|
-
}
|
|
727
|
-
getMetadata() {
|
|
728
|
-
return {
|
|
729
|
-
provider: "mistral",
|
|
730
|
-
model: this.config.model || "mistral-large-latest"
|
|
731
|
-
};
|
|
732
|
-
}
|
|
733
|
-
};
|
|
734
|
-
|
|
735
|
-
// src/llm/groq.ts
|
|
736
|
-
var GroqAdapter = class extends BaseLLMAdapter {
|
|
737
|
-
constructor(config) {
|
|
738
|
-
super(config);
|
|
739
|
-
}
|
|
740
|
-
async chat(messages) {
|
|
741
|
-
const res = await fetch("https://api.groq.com/openai/v1/chat/completions", {
|
|
742
|
-
method: "POST",
|
|
743
|
-
headers: {
|
|
744
|
-
"Content-Type": "application/json",
|
|
745
|
-
Authorization: `Bearer ${this.config.apiKey}`
|
|
746
|
-
},
|
|
747
|
-
body: JSON.stringify({
|
|
748
|
-
model: this.config.model || "llama-3.3-70b-versatile",
|
|
749
|
-
messages: messages.map((m) => ({ role: m.role, content: m.content })),
|
|
750
|
-
temperature: this.getTemperature(),
|
|
751
|
-
max_tokens: this.getMaxTokens()
|
|
752
|
-
})
|
|
753
|
-
});
|
|
754
|
-
if (!res.ok) {
|
|
755
|
-
const body = await res.text();
|
|
756
|
-
throw new Error(`Groq API error ${res.status}: ${body}`);
|
|
757
|
-
}
|
|
758
|
-
const data = await res.json();
|
|
759
|
-
return {
|
|
760
|
-
content: data.choices[0]?.message?.content || "",
|
|
761
|
-
tokens: data.usage ? { input: data.usage.prompt_tokens, output: data.usage.completion_tokens } : void 0
|
|
762
|
-
};
|
|
763
|
-
}
|
|
764
|
-
getMetadata() {
|
|
765
|
-
return {
|
|
766
|
-
provider: "groq",
|
|
767
|
-
model: this.config.model || "llama-3.3-70b-versatile"
|
|
768
|
-
};
|
|
769
|
-
}
|
|
770
|
-
};
|
|
771
|
-
|
|
772
|
-
// src/llm/together.ts
|
|
773
|
-
var TogetherAdapter = class extends BaseLLMAdapter {
|
|
774
|
-
constructor(config) {
|
|
775
|
-
super(config);
|
|
776
|
-
}
|
|
777
|
-
async chat(messages) {
|
|
778
|
-
const res = await fetch("https://api.together.xyz/v1/chat/completions", {
|
|
779
|
-
method: "POST",
|
|
780
|
-
headers: {
|
|
781
|
-
"Content-Type": "application/json",
|
|
782
|
-
Authorization: `Bearer ${this.config.apiKey}`
|
|
783
|
-
},
|
|
784
|
-
body: JSON.stringify({
|
|
785
|
-
model: this.config.model || "meta-llama/Llama-3.3-70B-Instruct-Turbo",
|
|
786
|
-
messages: messages.map((m) => ({ role: m.role, content: m.content })),
|
|
787
|
-
temperature: this.getTemperature(),
|
|
788
|
-
max_tokens: this.getMaxTokens()
|
|
789
|
-
})
|
|
790
|
-
});
|
|
791
|
-
if (!res.ok) {
|
|
792
|
-
const body = await res.text();
|
|
793
|
-
throw new Error(`Together API error ${res.status}: ${body}`);
|
|
794
|
-
}
|
|
795
|
-
const data = await res.json();
|
|
796
|
-
return {
|
|
797
|
-
content: data.choices[0]?.message?.content || "",
|
|
798
|
-
tokens: data.usage ? { input: data.usage.prompt_tokens, output: data.usage.completion_tokens } : void 0
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
|
-
getMetadata() {
|
|
802
|
-
return {
|
|
803
|
-
provider: "together",
|
|
804
|
-
model: this.config.model || "meta-llama/Llama-3.3-70B-Instruct-Turbo"
|
|
805
|
-
};
|
|
806
|
-
}
|
|
807
|
-
};
|
|
808
|
-
|
|
809
|
-
// src/llm/ollama.ts
|
|
810
|
-
var OllamaAdapter = class extends BaseLLMAdapter {
|
|
811
|
-
endpoint;
|
|
812
|
-
constructor(config) {
|
|
813
|
-
super(config);
|
|
814
|
-
this.endpoint = config.endpoint || "http://localhost:11434";
|
|
815
|
-
}
|
|
816
|
-
async chat(messages) {
|
|
817
|
-
const res = await fetch(`${this.endpoint}/api/chat`, {
|
|
818
|
-
method: "POST",
|
|
819
|
-
headers: { "Content-Type": "application/json" },
|
|
820
|
-
body: JSON.stringify({
|
|
821
|
-
model: this.config.model || "llama3.3",
|
|
822
|
-
messages: messages.map((m) => ({ role: m.role, content: m.content })),
|
|
823
|
-
stream: false,
|
|
824
|
-
options: {
|
|
825
|
-
temperature: this.getTemperature(),
|
|
826
|
-
num_predict: this.getMaxTokens()
|
|
827
|
-
}
|
|
828
|
-
})
|
|
829
|
-
});
|
|
830
|
-
if (!res.ok) {
|
|
831
|
-
const body = await res.text();
|
|
832
|
-
throw new Error(`Ollama error ${res.status}: ${body}`);
|
|
833
|
-
}
|
|
834
|
-
const data = await res.json();
|
|
835
|
-
return {
|
|
836
|
-
content: data.message?.content || "",
|
|
837
|
-
tokens: data.prompt_eval_count ? { input: data.prompt_eval_count, output: data.eval_count || 0 } : void 0
|
|
838
|
-
};
|
|
839
|
-
}
|
|
840
|
-
getMetadata() {
|
|
841
|
-
return {
|
|
842
|
-
provider: "ollama",
|
|
843
|
-
model: this.config.model || "llama3.3"
|
|
844
|
-
};
|
|
845
|
-
}
|
|
846
|
-
};
|
|
847
|
-
|
|
848
|
-
// src/llm/index.ts
|
|
849
|
-
function createLLMAdapter(config) {
|
|
850
|
-
switch (config.provider) {
|
|
851
|
-
case "openai":
|
|
852
|
-
return new OpenAIAdapter(config);
|
|
853
|
-
case "anthropic":
|
|
854
|
-
return new AnthropicAdapter(config);
|
|
855
|
-
case "google":
|
|
856
|
-
return new GoogleAdapter(config);
|
|
857
|
-
case "deepseek":
|
|
858
|
-
return new DeepSeekAdapter(config);
|
|
859
|
-
case "mistral":
|
|
860
|
-
return new MistralAdapter(config);
|
|
861
|
-
case "groq":
|
|
862
|
-
return new GroqAdapter(config);
|
|
863
|
-
case "together":
|
|
864
|
-
return new TogetherAdapter(config);
|
|
865
|
-
case "ollama":
|
|
866
|
-
return new OllamaAdapter(config);
|
|
867
|
-
default:
|
|
868
|
-
throw new Error(`Unknown LLM provider: ${config.provider}`);
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
// src/strategy/templates.ts
|
|
873
|
-
var templates = [
|
|
874
|
-
{
|
|
875
|
-
id: "momentum",
|
|
876
|
-
name: "Momentum Trader",
|
|
877
|
-
description: "Identifies tokens with strong upward price momentum and rides the trend. Uses LLM to analyze price action and market sentiment.",
|
|
878
|
-
category: "momentum",
|
|
879
|
-
venues: ["hyperliquid_perp", "hyperliquid_spot"],
|
|
880
|
-
riskLevel: "moderate",
|
|
881
|
-
systemPrompt: `You are a momentum trading agent. Analyze the provided market data and identify tokens with strong upward momentum.
|
|
882
|
-
|
|
883
|
-
Rules:
|
|
884
|
-
- Only trade tokens with clear directional momentum (avoid choppy markets)
|
|
885
|
-
- Use trailing stops to protect gains
|
|
886
|
-
- Size positions based on conviction (higher confidence = larger position)
|
|
887
|
-
- Cut losses quickly if momentum reverses
|
|
888
|
-
- Consider volume as confirmation of momentum
|
|
889
|
-
|
|
890
|
-
Return a JSON array of trade signals.`,
|
|
891
|
-
code: `
|
|
892
|
-
const prices = context.market.getPrices();
|
|
893
|
-
const positions = context.position.openPositions;
|
|
894
|
-
|
|
895
|
-
const messages = [
|
|
896
|
-
{ 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.' },
|
|
897
|
-
{ role: 'user', content: 'Current prices: ' + JSON.stringify(prices) + '\\nOpen positions: ' + JSON.stringify(positions.map(p => p.token)) + '\\nAnalyze for momentum opportunities.' }
|
|
898
|
-
];
|
|
899
|
-
|
|
900
|
-
const response = await context.llm.chat(messages);
|
|
901
|
-
try {
|
|
902
|
-
const signals = JSON.parse(response.content);
|
|
903
|
-
return Array.isArray(signals) ? signals : [];
|
|
904
|
-
} catch {
|
|
905
|
-
return [];
|
|
906
|
-
}
|
|
907
|
-
`
|
|
908
|
-
},
|
|
909
|
-
{
|
|
910
|
-
id: "value",
|
|
911
|
-
name: "Value Investor",
|
|
912
|
-
description: "Identifies undervalued tokens based on fundamental analysis. Buys dips and holds for mean reversion.",
|
|
913
|
-
category: "value",
|
|
914
|
-
venues: ["hyperliquid_spot"],
|
|
915
|
-
riskLevel: "conservative",
|
|
916
|
-
systemPrompt: `You are a value investing agent. Identify tokens trading below their intrinsic value.
|
|
917
|
-
|
|
918
|
-
Rules:
|
|
919
|
-
- Focus on established tokens with strong fundamentals
|
|
920
|
-
- Buy on significant dips (>10% from recent highs)
|
|
921
|
-
- Hold positions longer (swing/position timeframe)
|
|
922
|
-
- Avoid chasing pumps
|
|
923
|
-
- Diversify across sectors
|
|
924
|
-
|
|
925
|
-
Return a JSON array of trade signals.`,
|
|
926
|
-
code: `
|
|
927
|
-
const prices = context.market.getPrices();
|
|
928
|
-
const positions = context.position.openPositions;
|
|
929
|
-
|
|
930
|
-
const messages = [
|
|
931
|
-
{ 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.' },
|
|
932
|
-
{ role: 'user', content: 'Current prices: ' + JSON.stringify(prices) + '\\nPositions: ' + JSON.stringify(positions.map(p => ({ token: p.token, entry: p.costBasisPerUnit }))) + '\\nLook for value opportunities.' }
|
|
933
|
-
];
|
|
934
|
-
|
|
935
|
-
const response = await context.llm.chat(messages);
|
|
936
|
-
try {
|
|
937
|
-
const signals = JSON.parse(response.content);
|
|
938
|
-
return Array.isArray(signals) ? signals : [];
|
|
939
|
-
} catch {
|
|
940
|
-
return [];
|
|
941
|
-
}
|
|
942
|
-
`
|
|
943
|
-
},
|
|
944
|
-
{
|
|
945
|
-
id: "arbitrage",
|
|
946
|
-
name: "Arbitrage Hunter",
|
|
947
|
-
description: "Scans for price discrepancies across venues and chains. Executes quickly to capture spreads.",
|
|
948
|
-
category: "arbitrage",
|
|
949
|
-
venues: ["hyperliquid_perp", "hyperliquid_spot", "uniswap", "aerodrome"],
|
|
950
|
-
riskLevel: "aggressive",
|
|
951
|
-
systemPrompt: `You are an arbitrage agent. Find price discrepancies between venues.
|
|
952
|
-
|
|
953
|
-
Rules:
|
|
954
|
-
- Speed is critical \u2014 execute quickly before spreads close
|
|
955
|
-
- Account for fees and slippage in profitability calculations
|
|
956
|
-
- Only trade when net profit > 0.5% after all costs
|
|
957
|
-
- Monitor cross-chain opportunities (CEX vs DEX spreads)
|
|
958
|
-
|
|
959
|
-
Return a JSON array of trade signals.`,
|
|
960
|
-
code: `
|
|
961
|
-
const prices = context.market.getPrices();
|
|
962
|
-
|
|
963
|
-
const messages = [
|
|
964
|
-
{ 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.' },
|
|
965
|
-
{ role: 'user', content: 'Market prices: ' + JSON.stringify(prices) + '\\nScan for cross-venue arbitrage opportunities.' }
|
|
966
|
-
];
|
|
967
|
-
|
|
968
|
-
const response = await context.llm.chat(messages);
|
|
969
|
-
try {
|
|
970
|
-
const signals = JSON.parse(response.content);
|
|
971
|
-
return Array.isArray(signals) ? signals : [];
|
|
972
|
-
} catch {
|
|
973
|
-
return [];
|
|
974
|
-
}
|
|
975
|
-
`
|
|
976
|
-
},
|
|
977
|
-
{
|
|
978
|
-
id: "hold",
|
|
979
|
-
name: "Hold (No Trading)",
|
|
980
|
-
description: "Passive strategy that makes no trades. Useful for monitoring only.",
|
|
981
|
-
category: "custom",
|
|
982
|
-
venues: [],
|
|
983
|
-
riskLevel: "conservative",
|
|
984
|
-
systemPrompt: "",
|
|
985
|
-
code: `return [];`
|
|
986
|
-
}
|
|
987
|
-
];
|
|
988
|
-
function getTemplate(id) {
|
|
989
|
-
return templates.find((t) => t.id === id);
|
|
990
|
-
}
|
|
991
|
-
function listTemplates() {
|
|
992
|
-
return [...templates];
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
// src/strategy/loader.ts
|
|
996
|
-
import { existsSync as existsSync3 } from "fs";
|
|
997
|
-
import { resolve } from "path";
|
|
998
|
-
async function loadStrategy(config) {
|
|
999
|
-
if (config.file) {
|
|
1000
|
-
return loadFromFile(config.file);
|
|
1001
|
-
}
|
|
1002
|
-
if (config.template) {
|
|
1003
|
-
const template = getTemplate(config.template);
|
|
1004
|
-
if (!template) {
|
|
1005
|
-
throw new Error(`Unknown strategy template: ${config.template}. Available: momentum, value, arbitrage, hold`);
|
|
1006
|
-
}
|
|
1007
|
-
return loadFromCode(template.code);
|
|
1008
|
-
}
|
|
1009
|
-
return holdStrategy;
|
|
1010
|
-
}
|
|
1011
|
-
async function loadFromFile(filePath) {
|
|
1012
|
-
const resolved = resolve(filePath);
|
|
1013
|
-
if (!existsSync3(resolved)) {
|
|
1014
|
-
throw new Error(`Strategy file not found: ${resolved}`);
|
|
1015
|
-
}
|
|
1016
|
-
try {
|
|
1017
|
-
const mod = await import(resolved);
|
|
1018
|
-
const fn = mod.default || mod.strategy;
|
|
1019
|
-
if (typeof fn !== "function") {
|
|
1020
|
-
throw new Error(`Strategy file must export a default function or 'strategy' function`);
|
|
1021
|
-
}
|
|
1022
|
-
return fn;
|
|
1023
|
-
} catch (err) {
|
|
1024
|
-
throw new Error(`Failed to load strategy from ${resolved}: ${err.message}`);
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
async function loadFromCode(code) {
|
|
1028
|
-
const AsyncFunction = Object.getPrototypeOf(async function() {
|
|
1029
|
-
}).constructor;
|
|
1030
|
-
const fn = new AsyncFunction("context", code);
|
|
1031
|
-
return fn;
|
|
1032
|
-
}
|
|
1033
|
-
var holdStrategy = async (_context) => {
|
|
1034
|
-
return [];
|
|
1035
|
-
};
|
|
1036
|
-
|
|
1037
|
-
// src/trading/risk.ts
|
|
1038
|
-
var RiskManager = class {
|
|
1039
|
-
params;
|
|
1040
|
-
dailyPnL = 0;
|
|
1041
|
-
dailyFees = 0;
|
|
1042
|
-
lastResetDate = "";
|
|
1043
|
-
initialCapitalUSD;
|
|
1044
|
-
constructor(params, initialCapitalUSD = 1e4) {
|
|
1045
|
-
this.params = params;
|
|
1046
|
-
this.initialCapitalUSD = initialCapitalUSD;
|
|
1047
|
-
this.resetIfNewDay();
|
|
1048
|
-
}
|
|
1049
|
-
filterSignals(signals, market, openPositionCount) {
|
|
1050
|
-
this.resetIfNewDay();
|
|
1051
|
-
if (this.isDailyLossLimitHit()) {
|
|
1052
|
-
console.log("[risk] Daily loss limit hit \u2014 blocking all trades");
|
|
1053
|
-
return [];
|
|
1054
|
-
}
|
|
1055
|
-
return signals.filter((signal) => this.validateSignal(signal, market, openPositionCount));
|
|
1056
|
-
}
|
|
1057
|
-
validateSignal(signal, market, openPositionCount) {
|
|
1058
|
-
const threshold = this.params.confidenceThreshold ?? 0.5;
|
|
1059
|
-
if (signal.confidence !== void 0 && signal.confidence < threshold) {
|
|
1060
|
-
console.log(`[risk] Blocked ${signal.symbol}: confidence ${signal.confidence} < ${threshold}`);
|
|
1061
|
-
return false;
|
|
1062
|
-
}
|
|
1063
|
-
const tradeValue = signal.size * signal.price;
|
|
1064
|
-
const maxPositionValue = this.params.maxPositionSizeBps / 1e4 * this.initialCapitalUSD;
|
|
1065
|
-
if (tradeValue > maxPositionValue) {
|
|
1066
|
-
console.log(`[risk] Blocked ${signal.symbol}: trade $${tradeValue.toFixed(0)} > max $${maxPositionValue.toFixed(0)}`);
|
|
1067
|
-
return false;
|
|
1068
|
-
}
|
|
1069
|
-
if (tradeValue < this.params.minTradeValueUSD) {
|
|
1070
|
-
console.log(`[risk] Blocked ${signal.symbol}: trade $${tradeValue.toFixed(2)} < min $${this.params.minTradeValueUSD}`);
|
|
1071
|
-
return false;
|
|
1072
|
-
}
|
|
1073
|
-
if ((signal.side === "buy" || signal.side === "long") && openPositionCount >= this.params.maxConcurrentPositions) {
|
|
1074
|
-
console.log(`[risk] Blocked ${signal.symbol}: ${openPositionCount} positions >= max ${this.params.maxConcurrentPositions}`);
|
|
1075
|
-
return false;
|
|
1076
|
-
}
|
|
1077
|
-
if (signal.orderType === "market") {
|
|
1078
|
-
const currentPrice = market.getPrice(signal.symbol);
|
|
1079
|
-
if (currentPrice) {
|
|
1080
|
-
const slippageBps = Math.abs(signal.price - currentPrice) / currentPrice * 1e4;
|
|
1081
|
-
if (slippageBps > this.params.maxSlippageBps) {
|
|
1082
|
-
console.log(`[risk] Blocked ${signal.symbol}: slippage ${slippageBps.toFixed(0)}bps > max ${this.params.maxSlippageBps}bps`);
|
|
1083
|
-
return false;
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
return true;
|
|
1088
|
-
}
|
|
1089
|
-
recordTrade(pnl, fee) {
|
|
1090
|
-
this.dailyPnL += pnl;
|
|
1091
|
-
this.dailyFees += fee;
|
|
1092
|
-
}
|
|
1093
|
-
isDailyLossLimitHit() {
|
|
1094
|
-
const limit = this.params.maxDailyLossBps / 1e4 * this.initialCapitalUSD;
|
|
1095
|
-
return this.dailyPnL < -limit;
|
|
1096
|
-
}
|
|
1097
|
-
getDailyPnL() {
|
|
1098
|
-
return this.dailyPnL;
|
|
1099
|
-
}
|
|
1100
|
-
getDailyLossLimit() {
|
|
1101
|
-
return this.params.maxDailyLossBps / 1e4 * this.initialCapitalUSD;
|
|
1102
|
-
}
|
|
1103
|
-
resetIfNewDay() {
|
|
1104
|
-
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1105
|
-
if (today !== this.lastResetDate) {
|
|
1106
|
-
this.dailyPnL = 0;
|
|
1107
|
-
this.dailyFees = 0;
|
|
1108
|
-
this.lastResetDate = today;
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
};
|
|
1112
|
-
|
|
1113
|
-
// src/trading/market.ts
|
|
1114
|
-
var MarketDataService = class {
|
|
1115
|
-
prices = {};
|
|
1116
|
-
lastFetch = 0;
|
|
1117
|
-
cacheTTL;
|
|
1118
|
-
constructor(cacheTTLMs = 3e4) {
|
|
1119
|
-
this.cacheTTL = cacheTTLMs;
|
|
1120
|
-
}
|
|
1121
|
-
async refreshPrices(symbols) {
|
|
1122
|
-
if (symbols.length === 0) return this.prices;
|
|
1123
|
-
const now = Date.now();
|
|
1124
|
-
if (now - this.lastFetch < this.cacheTTL && Object.keys(this.prices).length > 0) {
|
|
1125
|
-
return this.prices;
|
|
1126
|
-
}
|
|
1127
|
-
try {
|
|
1128
|
-
const ids = symbols.map((s) => s.toLowerCase()).join(",");
|
|
1129
|
-
const res = await fetch(
|
|
1130
|
-
`https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd`
|
|
1131
|
-
);
|
|
1132
|
-
if (res.ok) {
|
|
1133
|
-
const data = await res.json();
|
|
1134
|
-
for (const [id, price] of Object.entries(data)) {
|
|
1135
|
-
this.prices[id.toUpperCase()] = price.usd;
|
|
1136
|
-
}
|
|
1137
|
-
this.lastFetch = now;
|
|
1138
|
-
}
|
|
1139
|
-
} catch (err) {
|
|
1140
|
-
console.warn("[market] Price fetch failed:", err.message);
|
|
1141
|
-
}
|
|
1142
|
-
return this.prices;
|
|
1143
|
-
}
|
|
1144
|
-
getPrice(symbol) {
|
|
1145
|
-
return this.prices[symbol.toUpperCase()];
|
|
1146
|
-
}
|
|
1147
|
-
getPrices() {
|
|
1148
|
-
return { ...this.prices };
|
|
1149
|
-
}
|
|
1150
|
-
setPrice(symbol, price) {
|
|
1151
|
-
this.prices[symbol.toUpperCase()] = price;
|
|
1152
|
-
}
|
|
1153
|
-
setPrices(prices) {
|
|
1154
|
-
for (const [symbol, price] of Object.entries(prices)) {
|
|
1155
|
-
this.prices[symbol.toUpperCase()] = price;
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
getOHLCV(_symbol, _timeframe) {
|
|
1159
|
-
return [];
|
|
1160
|
-
}
|
|
1161
|
-
};
|
|
1162
|
-
|
|
1163
|
-
// src/paper/executor.ts
|
|
1164
|
-
import { randomUUID } from "crypto";
|
|
1165
|
-
var PaperExecutor = class {
|
|
1166
|
-
cash;
|
|
1167
|
-
positions = /* @__PURE__ */ new Map();
|
|
1168
|
-
trades = [];
|
|
1169
|
-
equityCurve = [];
|
|
1170
|
-
slippageBps;
|
|
1171
|
-
feeRate;
|
|
1172
|
-
peakEquity;
|
|
1173
|
-
maxDrawdown = 0;
|
|
1174
|
-
constructor(initialCash = 1e4, slippageBps = 50, feeRate = 2e-3) {
|
|
1175
|
-
this.cash = initialCash;
|
|
1176
|
-
this.slippageBps = slippageBps;
|
|
1177
|
-
this.feeRate = feeRate;
|
|
1178
|
-
this.peakEquity = initialCash;
|
|
1179
|
-
this.recordEquity();
|
|
1180
|
-
}
|
|
1181
|
-
execute(signal, market) {
|
|
1182
|
-
const marketPrice = market.getPrice(signal.symbol);
|
|
1183
|
-
if (!marketPrice) {
|
|
1184
|
-
console.warn(`[paper] No price for ${signal.symbol}`);
|
|
1185
|
-
return null;
|
|
1186
|
-
}
|
|
1187
|
-
const isBuy = signal.side === "buy" || signal.side === "long";
|
|
1188
|
-
const slippage = this.slippageBps / 1e4 * marketPrice;
|
|
1189
|
-
const executedPrice = isBuy ? marketPrice + slippage : marketPrice - slippage;
|
|
1190
|
-
const fee = signal.size * executedPrice * this.feeRate;
|
|
1191
|
-
if (isBuy) {
|
|
1192
|
-
const totalCost = signal.size * executedPrice + fee;
|
|
1193
|
-
if (totalCost > this.cash) {
|
|
1194
|
-
console.warn(`[paper] Insufficient funds: need $${totalCost.toFixed(2)}, have $${this.cash.toFixed(2)}`);
|
|
1195
|
-
return null;
|
|
1196
|
-
}
|
|
1197
|
-
this.cash -= totalCost;
|
|
1198
|
-
const existing = this.positions.get(signal.symbol);
|
|
1199
|
-
if (existing) {
|
|
1200
|
-
const totalQty = existing.quantity + signal.size;
|
|
1201
|
-
existing.avgPrice = (existing.avgPrice * existing.quantity + executedPrice * signal.size) / totalQty;
|
|
1202
|
-
existing.quantity = totalQty;
|
|
1203
|
-
} else {
|
|
1204
|
-
this.positions.set(signal.symbol, { quantity: signal.size, avgPrice: executedPrice });
|
|
1205
|
-
}
|
|
1206
|
-
} else {
|
|
1207
|
-
const existing = this.positions.get(signal.symbol);
|
|
1208
|
-
if (!existing || existing.quantity < signal.size) {
|
|
1209
|
-
console.warn(`[paper] No position to sell: ${signal.symbol}`);
|
|
1210
|
-
return null;
|
|
1211
|
-
}
|
|
1212
|
-
const proceeds = signal.size * executedPrice - fee;
|
|
1213
|
-
this.cash += proceeds;
|
|
1214
|
-
existing.quantity -= signal.size;
|
|
1215
|
-
if (existing.quantity <= 1e-6) {
|
|
1216
|
-
this.positions.delete(signal.symbol);
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
const trade = {
|
|
1220
|
-
id: randomUUID(),
|
|
1221
|
-
symbol: signal.symbol,
|
|
1222
|
-
side: isBuy ? "buy" : "sell",
|
|
1223
|
-
size: signal.size,
|
|
1224
|
-
entryPrice: executedPrice,
|
|
1225
|
-
fee,
|
|
1226
|
-
timestamp: Date.now(),
|
|
1227
|
-
venue: `paper_${signal.venue}`
|
|
1228
|
-
};
|
|
1229
|
-
this.trades.push(trade);
|
|
1230
|
-
this.recordEquity();
|
|
1231
|
-
return trade;
|
|
1232
|
-
}
|
|
1233
|
-
recordEquity() {
|
|
1234
|
-
const equity = this.getEquity();
|
|
1235
|
-
this.equityCurve.push({ timestamp: Date.now(), equity });
|
|
1236
|
-
if (equity > this.peakEquity) this.peakEquity = equity;
|
|
1237
|
-
const drawdown = (this.peakEquity - equity) / this.peakEquity;
|
|
1238
|
-
if (drawdown > this.maxDrawdown) this.maxDrawdown = drawdown;
|
|
1239
|
-
}
|
|
1240
|
-
getEquity() {
|
|
1241
|
-
let positionValue = 0;
|
|
1242
|
-
for (const [, pos] of this.positions) {
|
|
1243
|
-
positionValue += pos.quantity * pos.avgPrice;
|
|
1244
|
-
}
|
|
1245
|
-
return this.cash + positionValue;
|
|
1246
|
-
}
|
|
1247
|
-
getCash() {
|
|
1248
|
-
return this.cash;
|
|
1249
|
-
}
|
|
1250
|
-
getPositions() {
|
|
1251
|
-
return new Map(this.positions);
|
|
1252
|
-
}
|
|
1253
|
-
getTrades() {
|
|
1254
|
-
return [...this.trades];
|
|
1255
|
-
}
|
|
1256
|
-
getEquityCurve() {
|
|
1257
|
-
return [...this.equityCurve];
|
|
1258
|
-
}
|
|
1259
|
-
getMetrics() {
|
|
1260
|
-
const initialEquity = this.equityCurve[0]?.equity || 0;
|
|
1261
|
-
const currentEquity = this.getEquity();
|
|
1262
|
-
const totalReturn = initialEquity > 0 ? (currentEquity - initialEquity) / initialEquity : 0;
|
|
1263
|
-
const dailyReturns = this.calculateDailyReturns();
|
|
1264
|
-
const sharpeRatio = this.calculateSharpe(dailyReturns);
|
|
1265
|
-
let wins = 0;
|
|
1266
|
-
let losses = 0;
|
|
1267
|
-
let grossProfit = 0;
|
|
1268
|
-
let grossLoss = 0;
|
|
1269
|
-
const sellTrades = this.trades.filter((t) => t.side === "sell");
|
|
1270
|
-
for (const sell of sellTrades) {
|
|
1271
|
-
const buyTrades = this.trades.filter((t) => t.side === "buy" && t.symbol === sell.symbol);
|
|
1272
|
-
const avgEntry = buyTrades.reduce((sum, t) => sum + t.entryPrice, 0) / (buyTrades.length || 1);
|
|
1273
|
-
const pnl = (sell.entryPrice - avgEntry) * sell.size - sell.fee;
|
|
1274
|
-
if (pnl > 0) {
|
|
1275
|
-
wins++;
|
|
1276
|
-
grossProfit += pnl;
|
|
1277
|
-
} else {
|
|
1278
|
-
losses++;
|
|
1279
|
-
grossLoss += Math.abs(pnl);
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
return {
|
|
1283
|
-
totalReturn,
|
|
1284
|
-
sharpeRatio,
|
|
1285
|
-
maxDrawdown: this.maxDrawdown,
|
|
1286
|
-
winRate: wins + losses > 0 ? wins / (wins + losses) : 0,
|
|
1287
|
-
profitFactor: grossLoss > 0 ? grossProfit / grossLoss : grossProfit > 0 ? Infinity : 0,
|
|
1288
|
-
totalTrades: this.trades.length
|
|
1289
|
-
};
|
|
1290
|
-
}
|
|
1291
|
-
calculateDailyReturns() {
|
|
1292
|
-
if (this.equityCurve.length < 2) return [];
|
|
1293
|
-
const dailyEquity = /* @__PURE__ */ new Map();
|
|
1294
|
-
for (const point of this.equityCurve) {
|
|
1295
|
-
const date = new Date(point.timestamp).toISOString().slice(0, 10);
|
|
1296
|
-
dailyEquity.set(date, point.equity);
|
|
1297
|
-
}
|
|
1298
|
-
const dates = Array.from(dailyEquity.keys()).sort();
|
|
1299
|
-
const returns = [];
|
|
1300
|
-
for (let i = 1; i < dates.length; i++) {
|
|
1301
|
-
const prev = dailyEquity.get(dates[i - 1]);
|
|
1302
|
-
const curr = dailyEquity.get(dates[i]);
|
|
1303
|
-
if (prev > 0) returns.push((curr - prev) / prev);
|
|
1304
|
-
}
|
|
1305
|
-
return returns;
|
|
1306
|
-
}
|
|
1307
|
-
calculateSharpe(returns) {
|
|
1308
|
-
if (returns.length < 2) return 0;
|
|
1309
|
-
const mean = returns.reduce((a, b) => a + b, 0) / returns.length;
|
|
1310
|
-
const variance = returns.reduce((sum, r) => sum + (r - mean) ** 2, 0) / (returns.length - 1);
|
|
1311
|
-
const std = Math.sqrt(variance);
|
|
1312
|
-
if (std === 0) return 0;
|
|
1313
|
-
return mean / std * Math.sqrt(365);
|
|
1314
|
-
}
|
|
1315
|
-
};
|
|
1316
|
-
|
|
1317
|
-
// src/runtime.ts
|
|
1318
|
-
var SDK_VERSION = "0.1.0";
|
|
1319
|
-
var AgentRuntime = class {
|
|
1320
|
-
config;
|
|
1321
|
-
relay;
|
|
1322
|
-
signal;
|
|
1323
|
-
store;
|
|
1324
|
-
positions;
|
|
1325
|
-
llm;
|
|
1326
|
-
strategy = null;
|
|
1327
|
-
risk;
|
|
1328
|
-
market;
|
|
1329
|
-
paper = null;
|
|
1330
|
-
mode = "idle";
|
|
1331
|
-
cycleCount = 0;
|
|
1332
|
-
lastCycleAt = 0;
|
|
1333
|
-
tradingInterval = null;
|
|
1334
|
-
running = false;
|
|
1335
|
-
constructor(config) {
|
|
1336
|
-
this.config = config;
|
|
1337
|
-
this.store = new FileStore(`data/${config.agentId}-store.json`);
|
|
1338
|
-
this.positions = new PositionTracker(this.store);
|
|
1339
|
-
this.market = new MarketDataService();
|
|
1340
|
-
this.risk = new RiskManager(
|
|
1341
|
-
{
|
|
1342
|
-
maxPositionSizeBps: config.trading.maxPositionSizeBps,
|
|
1343
|
-
maxDailyLossBps: config.trading.maxDailyLossBps,
|
|
1344
|
-
maxConcurrentPositions: config.trading.maxConcurrentPositions,
|
|
1345
|
-
maxSlippageBps: config.trading.maxSlippageBps,
|
|
1346
|
-
minTradeValueUSD: config.trading.minTradeValueUSD
|
|
1347
|
-
},
|
|
1348
|
-
config.trading.initialCapitalUSD
|
|
1349
|
-
);
|
|
1350
|
-
this.llm = createLLMAdapter(config.llm);
|
|
1351
|
-
this.relay = new RelayClient({
|
|
1352
|
-
url: config.relay.url,
|
|
1353
|
-
agentId: config.agentId,
|
|
1354
|
-
token: config.apiToken,
|
|
1355
|
-
heartbeatIntervalMs: config.relay.heartbeatIntervalMs,
|
|
1356
|
-
reconnectMaxAttempts: config.relay.reconnectMaxAttempts,
|
|
1357
|
-
onCommand: (cmd) => this.handleCommand(cmd),
|
|
1358
|
-
onConnected: () => {
|
|
1359
|
-
console.log(`[runtime] Connected to command center`);
|
|
1360
|
-
this.sendStatus();
|
|
1361
|
-
},
|
|
1362
|
-
onDisconnected: () => {
|
|
1363
|
-
console.log(`[runtime] Disconnected from command center`);
|
|
1364
|
-
}
|
|
1365
|
-
});
|
|
1366
|
-
this.signal = new SignalReporter(this.relay);
|
|
1367
|
-
if (config.trading.mode === "paper") {
|
|
1368
|
-
this.paper = new PaperExecutor(config.trading.initialCapitalUSD ?? 1e4);
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
async start() {
|
|
1372
|
-
console.log(`[runtime] Starting agent ${this.config.agentId}`);
|
|
1373
|
-
console.log(`[runtime] Mode: ${this.config.trading.mode}`);
|
|
1374
|
-
console.log(`[runtime] LLM: ${this.config.llm.provider}/${this.config.llm.model}`);
|
|
1375
|
-
try {
|
|
1376
|
-
this.strategy = await loadStrategy(this.config.strategy);
|
|
1377
|
-
console.log(`[runtime] Strategy loaded`);
|
|
1378
|
-
} catch (err) {
|
|
1379
|
-
console.error(`[runtime] Failed to load strategy:`, err.message);
|
|
1380
|
-
this.strategy = null;
|
|
1381
|
-
}
|
|
1382
|
-
try {
|
|
1383
|
-
await this.relay.connect();
|
|
1384
|
-
this.signal.reportInfo("Agent Started", `Agent ${this.config.agentId} connected to command center`);
|
|
1385
|
-
} catch (err) {
|
|
1386
|
-
console.error(`[runtime] Failed to connect to relay:`, err.message);
|
|
1387
|
-
console.log(`[runtime] Will retry in background...`);
|
|
1388
|
-
}
|
|
1389
|
-
this.running = true;
|
|
1390
|
-
this.mode = "idle";
|
|
1391
|
-
this.sendStatus();
|
|
1392
|
-
if (this.config.trading.mode === "paper" || this.config.trading.mode === "live") {
|
|
1393
|
-
this.startTrading();
|
|
1394
|
-
}
|
|
1395
|
-
process.on("SIGTERM", () => this.stop());
|
|
1396
|
-
process.on("SIGINT", () => this.stop());
|
|
1397
|
-
}
|
|
1398
|
-
async stop() {
|
|
1399
|
-
console.log(`[runtime] Stopping agent ${this.config.agentId}`);
|
|
1400
|
-
this.running = false;
|
|
1401
|
-
this.stopTrading();
|
|
1402
|
-
this.signal.reportInfo("Agent Stopped", `Agent ${this.config.agentId} shutting down`);
|
|
1403
|
-
this.relay.disconnect();
|
|
1404
|
-
console.log(`[runtime] Agent stopped`);
|
|
1405
|
-
process.exit(0);
|
|
1406
|
-
}
|
|
1407
|
-
startTrading() {
|
|
1408
|
-
if (this.tradingInterval) return;
|
|
1409
|
-
this.mode = this.config.trading.mode === "paper" ? "paper" : "trading";
|
|
1410
|
-
console.log(`[runtime] Trading started (${this.mode}, interval: ${this.config.trading.tradingIntervalMs}ms)`);
|
|
1411
|
-
this.sendStatus();
|
|
1412
|
-
this.signal.reportInfo("Trading Started", `Mode: ${this.mode}`);
|
|
1413
|
-
this.runCycle();
|
|
1414
|
-
this.tradingInterval = setInterval(() => {
|
|
1415
|
-
this.runCycle();
|
|
1416
|
-
}, this.config.trading.tradingIntervalMs);
|
|
1417
|
-
}
|
|
1418
|
-
stopTrading() {
|
|
1419
|
-
if (this.tradingInterval) {
|
|
1420
|
-
clearInterval(this.tradingInterval);
|
|
1421
|
-
this.tradingInterval = null;
|
|
1422
|
-
}
|
|
1423
|
-
this.mode = "idle";
|
|
1424
|
-
this.sendStatus();
|
|
1425
|
-
console.log(`[runtime] Trading stopped`);
|
|
1426
|
-
}
|
|
1427
|
-
async runCycle() {
|
|
1428
|
-
if (!this.running || !this.strategy) return;
|
|
1429
|
-
this.cycleCount++;
|
|
1430
|
-
this.lastCycleAt = Date.now();
|
|
1431
|
-
try {
|
|
1432
|
-
const prices = this.market.getPrices();
|
|
1433
|
-
const positionSummary = this.positions.getSummary(prices);
|
|
1434
|
-
const signals = await this.strategy({
|
|
1435
|
-
llm: this.llm,
|
|
1436
|
-
market: this.market,
|
|
1437
|
-
position: positionSummary,
|
|
1438
|
-
store: this.store,
|
|
1439
|
-
config: this.config.trading,
|
|
1440
|
-
log: (msg) => {
|
|
1441
|
-
console.log(`[strategy] ${msg}`);
|
|
1442
|
-
this.signal.reportInfo("Strategy Log", msg);
|
|
1443
|
-
}
|
|
1444
|
-
});
|
|
1445
|
-
if (!Array.isArray(signals) || signals.length === 0) {
|
|
1446
|
-
this.sendStatus();
|
|
1447
|
-
return;
|
|
1448
|
-
}
|
|
1449
|
-
const filtered = this.risk.filterSignals(signals, this.market, positionSummary.openPositions.length);
|
|
1450
|
-
for (const sig of filtered) {
|
|
1451
|
-
try {
|
|
1452
|
-
if (this.paper) {
|
|
1453
|
-
const trade = this.paper.execute(sig, this.market);
|
|
1454
|
-
if (trade) {
|
|
1455
|
-
this.signal.reportTrade({
|
|
1456
|
-
...sig,
|
|
1457
|
-
price: trade.entryPrice,
|
|
1458
|
-
fee: trade.fee,
|
|
1459
|
-
venue: trade.venue,
|
|
1460
|
-
venueFillId: trade.id,
|
|
1461
|
-
venueTimestamp: new Date(trade.timestamp).toISOString()
|
|
1462
|
-
});
|
|
1463
|
-
}
|
|
1464
|
-
} else {
|
|
1465
|
-
console.log(`[runtime] Live trade signal: ${sig.side} ${sig.size} ${sig.symbol}`);
|
|
1466
|
-
}
|
|
1467
|
-
} catch (err) {
|
|
1468
|
-
console.error(`[runtime] Trade execution error:`, err.message);
|
|
1469
|
-
this.signal.reportError("Trade Error", err.message);
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
} catch (err) {
|
|
1473
|
-
console.error(`[runtime] Cycle error:`, err.message);
|
|
1474
|
-
this.signal.reportError("Cycle Error", err.message);
|
|
1475
|
-
this.mode = "idle";
|
|
1476
|
-
}
|
|
1477
|
-
this.sendStatus();
|
|
1478
|
-
}
|
|
1479
|
-
handleCommand(command) {
|
|
1480
|
-
console.log(`[runtime] Command received: ${command.type}`);
|
|
1481
|
-
switch (command.type) {
|
|
1482
|
-
case "start_trading":
|
|
1483
|
-
this.startTrading();
|
|
1484
|
-
break;
|
|
1485
|
-
case "stop_trading":
|
|
1486
|
-
this.stopTrading();
|
|
1487
|
-
break;
|
|
1488
|
-
case "update_risk_params":
|
|
1489
|
-
if (command.params) {
|
|
1490
|
-
console.log(`[runtime] Risk params updated`);
|
|
1491
|
-
}
|
|
1492
|
-
break;
|
|
1493
|
-
case "get_status":
|
|
1494
|
-
this.sendStatus();
|
|
1495
|
-
break;
|
|
1496
|
-
case "reload_config":
|
|
1497
|
-
console.log(`[runtime] Config reload requested`);
|
|
1498
|
-
break;
|
|
1499
|
-
default:
|
|
1500
|
-
console.warn(`[runtime] Unknown command: ${command.type}`);
|
|
1501
|
-
}
|
|
1502
|
-
}
|
|
1503
|
-
sendStatus() {
|
|
1504
|
-
const status = {
|
|
1505
|
-
mode: this.mode,
|
|
1506
|
-
agentId: this.config.agentId,
|
|
1507
|
-
sdkVersion: SDK_VERSION,
|
|
1508
|
-
cycleCount: this.cycleCount,
|
|
1509
|
-
lastCycleAt: this.lastCycleAt,
|
|
1510
|
-
tradingIntervalMs: this.config.trading.tradingIntervalMs,
|
|
1511
|
-
llm: {
|
|
1512
|
-
provider: this.config.llm.provider,
|
|
1513
|
-
model: this.config.llm.model || "default"
|
|
1514
|
-
},
|
|
1515
|
-
risk: {
|
|
1516
|
-
dailyPnL: this.risk.getDailyPnL(),
|
|
1517
|
-
dailyLossLimit: this.risk.getDailyLossLimit(),
|
|
1518
|
-
isLimitHit: this.risk.isDailyLossLimitHit()
|
|
1519
|
-
},
|
|
1520
|
-
positions: {
|
|
1521
|
-
openPositions: this.positions.getPositions().length,
|
|
1522
|
-
totalUnrealizedPnL: 0,
|
|
1523
|
-
totalRealizedPnL: this.positions.getRealizedPnL()
|
|
1524
|
-
}
|
|
1525
|
-
};
|
|
1526
|
-
if (this.paper) {
|
|
1527
|
-
const metrics = this.paper.getMetrics();
|
|
1528
|
-
status.paper = {
|
|
1529
|
-
active: true,
|
|
1530
|
-
simulatedValue: this.paper.getEquity(),
|
|
1531
|
-
simulatedPnLPct: metrics.totalReturn * 100
|
|
1532
|
-
};
|
|
1533
|
-
status.portfolioValue = this.paper.getEquity();
|
|
1534
|
-
}
|
|
1535
|
-
this.relay.sendHeartbeat(status);
|
|
1536
|
-
}
|
|
1537
|
-
};
|
|
1538
|
-
|
|
1539
|
-
export {
|
|
1540
|
-
loadConfig,
|
|
1541
|
-
generateSampleConfig,
|
|
1542
|
-
writeSampleConfig,
|
|
1543
|
-
RelayClient,
|
|
1544
|
-
SignalReporter,
|
|
1545
|
-
FileStore,
|
|
1546
|
-
PositionTracker,
|
|
1547
|
-
createLLMAdapter,
|
|
1548
|
-
getTemplate,
|
|
1549
|
-
listTemplates,
|
|
1550
|
-
loadStrategy,
|
|
1551
|
-
RiskManager,
|
|
1552
|
-
MarketDataService,
|
|
1553
|
-
PaperExecutor,
|
|
1554
|
-
AgentRuntime
|
|
1555
|
-
};
|