@exagent/agent 0.3.2 → 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-VDK4XPAC.js → chunk-WTECTX2Z.js} +31 -4
- package/dist/cli.js +212 -77
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli.ts +122 -3
- package/src/config.ts +19 -2
- package/src/runtime.ts +18 -2
- package/src/setup.ts +118 -67
- 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-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-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-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-CIEZAYOU.js
DELETED
|
@@ -1,4701 +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
|
-
spot: z.object({
|
|
52
|
-
enabled: z.boolean().default(false),
|
|
53
|
-
chains: z.array(z.string()).default(["base"]),
|
|
54
|
-
defaultChain: z.string().default("base"),
|
|
55
|
-
maxSlippageBps: z.number().min(1).max(1e3).default(50),
|
|
56
|
-
maxSwapValueUSD: z.number().default(1e4)
|
|
57
|
-
}).optional(),
|
|
58
|
-
bridge: z.object({
|
|
59
|
-
enabled: z.boolean().default(false),
|
|
60
|
-
defaultBridge: z.string().default("across"),
|
|
61
|
-
maxBridgeValueUSD: z.number().default(1e4),
|
|
62
|
-
fillTimeoutMs: z.number().default(3e5),
|
|
63
|
-
pollIntervalMs: z.number().default(2e3)
|
|
64
|
-
}).optional()
|
|
65
|
-
}).optional(),
|
|
66
|
-
relay: z.object({
|
|
67
|
-
url: z.string(),
|
|
68
|
-
heartbeatIntervalMs: z.number().default(3e4),
|
|
69
|
-
reconnectMaxAttempts: z.number().default(50)
|
|
70
|
-
})
|
|
71
|
-
});
|
|
72
|
-
function loadConfig(path = "agent-config.json") {
|
|
73
|
-
if (!existsSync(path)) {
|
|
74
|
-
throw new Error(`Config file not found: ${path}. Run 'exagent init' first.`);
|
|
75
|
-
}
|
|
76
|
-
const raw = readFileSync(path, "utf-8");
|
|
77
|
-
let parsed;
|
|
78
|
-
try {
|
|
79
|
-
parsed = JSON.parse(raw);
|
|
80
|
-
} catch {
|
|
81
|
-
throw new Error(`Invalid JSON in ${path}`);
|
|
82
|
-
}
|
|
83
|
-
const config = parsed;
|
|
84
|
-
const llm = config.llm || {};
|
|
85
|
-
if (process.env.EXAGENT_LLM_PROVIDER) llm.provider = process.env.EXAGENT_LLM_PROVIDER;
|
|
86
|
-
if (process.env.EXAGENT_LLM_MODEL) llm.model = process.env.EXAGENT_LLM_MODEL;
|
|
87
|
-
if (process.env.EXAGENT_LLM_API_KEY) llm.apiKey = process.env.EXAGENT_LLM_API_KEY;
|
|
88
|
-
if (process.env.EXAGENT_API_URL) config.apiUrl = process.env.EXAGENT_API_URL;
|
|
89
|
-
if (process.env.EXAGENT_API_TOKEN) config.apiToken = process.env.EXAGENT_API_TOKEN;
|
|
90
|
-
if (process.env.EXAGENT_WALLET_PRIVATE_KEY) {
|
|
91
|
-
config.wallet = { privateKey: process.env.EXAGENT_WALLET_PRIVATE_KEY };
|
|
92
|
-
}
|
|
93
|
-
config.llm = llm;
|
|
94
|
-
const result = configSchema.safeParse(config);
|
|
95
|
-
if (!result.success) {
|
|
96
|
-
const issues = result.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
97
|
-
throw new Error(`Invalid config:
|
|
98
|
-
${issues}`);
|
|
99
|
-
}
|
|
100
|
-
return result.data;
|
|
101
|
-
}
|
|
102
|
-
function generateSampleConfig(agentId, apiUrl) {
|
|
103
|
-
const config = {
|
|
104
|
-
agentId,
|
|
105
|
-
apiUrl,
|
|
106
|
-
apiToken: "<your-jwt-token>",
|
|
107
|
-
wallet: {
|
|
108
|
-
privateKey: "<your-hex-private-key>"
|
|
109
|
-
},
|
|
110
|
-
llm: {
|
|
111
|
-
provider: "openai",
|
|
112
|
-
model: "gpt-4o",
|
|
113
|
-
apiKey: "<your-api-key>"
|
|
114
|
-
},
|
|
115
|
-
strategy: {
|
|
116
|
-
template: "momentum"
|
|
117
|
-
},
|
|
118
|
-
trading: {
|
|
119
|
-
mode: "paper",
|
|
120
|
-
timeHorizon: "swing",
|
|
121
|
-
maxPositionSizeBps: 2e3,
|
|
122
|
-
maxDailyLossBps: 500,
|
|
123
|
-
maxConcurrentPositions: 5,
|
|
124
|
-
tradingIntervalMs: 6e4,
|
|
125
|
-
maxSlippageBps: 100,
|
|
126
|
-
minTradeValueUSD: 10,
|
|
127
|
-
initialCapitalUSD: 1e4
|
|
128
|
-
},
|
|
129
|
-
venues: {
|
|
130
|
-
hyperliquid_perp: {
|
|
131
|
-
enabled: false,
|
|
132
|
-
apiUrl: "https://api.hyperliquid.xyz",
|
|
133
|
-
wsUrl: "wss://api.hyperliquid.xyz/ws",
|
|
134
|
-
maxLeverage: 10,
|
|
135
|
-
maxNotionalUSD: 5e4
|
|
136
|
-
},
|
|
137
|
-
polymarket: {
|
|
138
|
-
enabled: false,
|
|
139
|
-
clobApiUrl: "https://clob.polymarket.com",
|
|
140
|
-
gammaApiUrl: "https://gamma-api.polymarket.com",
|
|
141
|
-
maxNotionalUSD: 1e3,
|
|
142
|
-
maxTotalExposureUSD: 5e3
|
|
143
|
-
},
|
|
144
|
-
spot: {
|
|
145
|
-
enabled: false,
|
|
146
|
-
chains: ["base"],
|
|
147
|
-
defaultChain: "base",
|
|
148
|
-
maxSlippageBps: 50,
|
|
149
|
-
maxSwapValueUSD: 1e4
|
|
150
|
-
},
|
|
151
|
-
bridge: {
|
|
152
|
-
enabled: false,
|
|
153
|
-
defaultBridge: "across",
|
|
154
|
-
maxBridgeValueUSD: 1e4,
|
|
155
|
-
fillTimeoutMs: 3e5,
|
|
156
|
-
pollIntervalMs: 2e3
|
|
157
|
-
}
|
|
158
|
-
},
|
|
159
|
-
relay: {
|
|
160
|
-
url: apiUrl.replace(/^http/, "ws") + "/ws/agent",
|
|
161
|
-
heartbeatIntervalMs: 3e4,
|
|
162
|
-
reconnectMaxAttempts: 50
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
return JSON.stringify(config, null, 2);
|
|
166
|
-
}
|
|
167
|
-
function writeSampleConfig(agentId, apiUrl, path = "agent-config.json") {
|
|
168
|
-
writeFileSync(path, generateSampleConfig(agentId, apiUrl));
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// src/chains.ts
|
|
172
|
-
import { base, arbitrum, polygon, mainnet } from "viem/chains";
|
|
173
|
-
var CHAIN_CONFIGS = {
|
|
174
|
-
base: {
|
|
175
|
-
chainId: 8453,
|
|
176
|
-
name: "base",
|
|
177
|
-
viemChain: base,
|
|
178
|
-
rpcUrl: "https://mainnet.base.org",
|
|
179
|
-
nativeCurrency: "ETH",
|
|
180
|
-
blockExplorer: "https://basescan.org",
|
|
181
|
-
wethAddress: "0x4200000000000000000000000000000000000006",
|
|
182
|
-
usdcAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
183
|
-
dexRouters: {
|
|
184
|
-
uniswap: "0x2626664c2603336E57B271c5C0b26F421741e481",
|
|
185
|
-
aerodrome: "0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43"
|
|
186
|
-
},
|
|
187
|
-
uniswapQuoter: "0x3d4e44Eb1374240CE5F1B871ab261CD16335B76a",
|
|
188
|
-
acrossSpokePool: "0x09aea4b2242abC8bb4BB78D537A67a245A7bEC64"
|
|
189
|
-
},
|
|
190
|
-
ethereum: {
|
|
191
|
-
chainId: 1,
|
|
192
|
-
name: "ethereum",
|
|
193
|
-
viemChain: mainnet,
|
|
194
|
-
rpcUrl: "https://eth.llamarpc.com",
|
|
195
|
-
nativeCurrency: "ETH",
|
|
196
|
-
blockExplorer: "https://etherscan.io",
|
|
197
|
-
wethAddress: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
|
198
|
-
usdcAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
|
199
|
-
dexRouters: {
|
|
200
|
-
uniswap: "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"
|
|
201
|
-
},
|
|
202
|
-
uniswapQuoter: "0x61fFE014bA17989E743c5F6cB21bF9697530B21e",
|
|
203
|
-
acrossSpokePool: "0x5c7BCd6E7De5423a257D81B442095A1a6ced35C5"
|
|
204
|
-
},
|
|
205
|
-
arbitrum: {
|
|
206
|
-
chainId: 42161,
|
|
207
|
-
name: "arbitrum",
|
|
208
|
-
viemChain: arbitrum,
|
|
209
|
-
rpcUrl: "https://arb1.arbitrum.io/rpc",
|
|
210
|
-
nativeCurrency: "ETH",
|
|
211
|
-
blockExplorer: "https://arbiscan.io",
|
|
212
|
-
wethAddress: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
|
|
213
|
-
usdcAddress: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
|
|
214
|
-
dexRouters: {
|
|
215
|
-
uniswap: "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"
|
|
216
|
-
},
|
|
217
|
-
uniswapQuoter: "0x61fFE014bA17989E743c5F6cB21bF9697530B21e",
|
|
218
|
-
acrossSpokePool: "0xe35e9842fceaCA96570B734083f4a58e8F7C5f2A"
|
|
219
|
-
},
|
|
220
|
-
polygon: {
|
|
221
|
-
chainId: 137,
|
|
222
|
-
name: "polygon",
|
|
223
|
-
viemChain: polygon,
|
|
224
|
-
rpcUrl: "https://polygon-bor-rpc.publicnode.com",
|
|
225
|
-
nativeCurrency: "POL",
|
|
226
|
-
blockExplorer: "https://polygonscan.com",
|
|
227
|
-
wethAddress: "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270",
|
|
228
|
-
usdcAddress: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
|
|
229
|
-
dexRouters: {
|
|
230
|
-
uniswap: "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"
|
|
231
|
-
},
|
|
232
|
-
uniswapQuoter: "0x61fFE014bA17989E743c5F6cB21bF9697530B21e",
|
|
233
|
-
acrossSpokePool: "0x9295ee1d8C5b022Be115A2AD3c30C72E34e7F096"
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
|
-
function getChainConfig(nameOrId) {
|
|
237
|
-
if (typeof nameOrId === "number") {
|
|
238
|
-
return Object.values(CHAIN_CONFIGS).find((c) => c.chainId === nameOrId);
|
|
239
|
-
}
|
|
240
|
-
return CHAIN_CONFIGS[nameOrId.toLowerCase()];
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// src/spot/types.ts
|
|
244
|
-
var DEFAULT_SPOT_CONFIG = {
|
|
245
|
-
enabled: false,
|
|
246
|
-
chains: ["base"],
|
|
247
|
-
defaultChain: "base",
|
|
248
|
-
maxSlippageBps: 50,
|
|
249
|
-
maxSwapValueUSD: 1e4
|
|
250
|
-
};
|
|
251
|
-
var ERC20_ABI = [
|
|
252
|
-
{
|
|
253
|
-
type: "function",
|
|
254
|
-
name: "approve",
|
|
255
|
-
inputs: [
|
|
256
|
-
{ name: "spender", type: "address" },
|
|
257
|
-
{ name: "amount", type: "uint256" }
|
|
258
|
-
],
|
|
259
|
-
outputs: [{ type: "bool" }],
|
|
260
|
-
stateMutability: "nonpayable"
|
|
261
|
-
},
|
|
262
|
-
{
|
|
263
|
-
type: "function",
|
|
264
|
-
name: "allowance",
|
|
265
|
-
inputs: [
|
|
266
|
-
{ name: "owner", type: "address" },
|
|
267
|
-
{ name: "spender", type: "address" }
|
|
268
|
-
],
|
|
269
|
-
outputs: [{ type: "uint256" }],
|
|
270
|
-
stateMutability: "view"
|
|
271
|
-
},
|
|
272
|
-
{
|
|
273
|
-
type: "function",
|
|
274
|
-
name: "balanceOf",
|
|
275
|
-
inputs: [{ name: "account", type: "address" }],
|
|
276
|
-
outputs: [{ type: "uint256" }],
|
|
277
|
-
stateMutability: "view"
|
|
278
|
-
},
|
|
279
|
-
{
|
|
280
|
-
type: "function",
|
|
281
|
-
name: "decimals",
|
|
282
|
-
inputs: [],
|
|
283
|
-
outputs: [{ type: "uint8" }],
|
|
284
|
-
stateMutability: "view"
|
|
285
|
-
},
|
|
286
|
-
{
|
|
287
|
-
type: "function",
|
|
288
|
-
name: "symbol",
|
|
289
|
-
inputs: [],
|
|
290
|
-
outputs: [{ type: "string" }],
|
|
291
|
-
stateMutability: "view"
|
|
292
|
-
},
|
|
293
|
-
{
|
|
294
|
-
type: "function",
|
|
295
|
-
name: "transfer",
|
|
296
|
-
inputs: [
|
|
297
|
-
{ name: "to", type: "address" },
|
|
298
|
-
{ name: "amount", type: "uint256" }
|
|
299
|
-
],
|
|
300
|
-
outputs: [{ type: "bool" }],
|
|
301
|
-
stateMutability: "nonpayable"
|
|
302
|
-
}
|
|
303
|
-
];
|
|
304
|
-
var UNISWAP_QUOTER_V2_ABI = [
|
|
305
|
-
{
|
|
306
|
-
type: "function",
|
|
307
|
-
name: "quoteExactInputSingle",
|
|
308
|
-
inputs: [
|
|
309
|
-
{
|
|
310
|
-
name: "params",
|
|
311
|
-
type: "tuple",
|
|
312
|
-
components: [
|
|
313
|
-
{ name: "tokenIn", type: "address" },
|
|
314
|
-
{ name: "tokenOut", type: "address" },
|
|
315
|
-
{ name: "amountIn", type: "uint256" },
|
|
316
|
-
{ name: "fee", type: "uint24" },
|
|
317
|
-
{ name: "sqrtPriceLimitX96", type: "uint160" }
|
|
318
|
-
]
|
|
319
|
-
}
|
|
320
|
-
],
|
|
321
|
-
outputs: [
|
|
322
|
-
{ name: "amountOut", type: "uint256" },
|
|
323
|
-
{ name: "sqrtPriceX96After", type: "uint160" },
|
|
324
|
-
{ name: "initializedTicksCrossed", type: "uint32" },
|
|
325
|
-
{ name: "gasEstimate", type: "uint256" }
|
|
326
|
-
],
|
|
327
|
-
stateMutability: "nonpayable"
|
|
328
|
-
}
|
|
329
|
-
];
|
|
330
|
-
var UNISWAP_SWAP_ROUTER_ABI = [
|
|
331
|
-
{
|
|
332
|
-
type: "function",
|
|
333
|
-
name: "exactInputSingle",
|
|
334
|
-
inputs: [
|
|
335
|
-
{
|
|
336
|
-
name: "params",
|
|
337
|
-
type: "tuple",
|
|
338
|
-
components: [
|
|
339
|
-
{ name: "tokenIn", type: "address" },
|
|
340
|
-
{ name: "tokenOut", type: "address" },
|
|
341
|
-
{ name: "fee", type: "uint24" },
|
|
342
|
-
{ name: "recipient", type: "address" },
|
|
343
|
-
{ name: "amountIn", type: "uint256" },
|
|
344
|
-
{ name: "amountOutMinimum", type: "uint256" },
|
|
345
|
-
{ name: "sqrtPriceLimitX96", type: "uint160" }
|
|
346
|
-
]
|
|
347
|
-
}
|
|
348
|
-
],
|
|
349
|
-
outputs: [{ name: "amountOut", type: "uint256" }],
|
|
350
|
-
stateMutability: "payable"
|
|
351
|
-
}
|
|
352
|
-
];
|
|
353
|
-
var AERODROME_ROUTER_ABI = [
|
|
354
|
-
{
|
|
355
|
-
type: "function",
|
|
356
|
-
name: "swapExactTokensForTokens",
|
|
357
|
-
inputs: [
|
|
358
|
-
{ name: "amountIn", type: "uint256" },
|
|
359
|
-
{ name: "amountOutMin", type: "uint256" },
|
|
360
|
-
{
|
|
361
|
-
name: "routes",
|
|
362
|
-
type: "tuple[]",
|
|
363
|
-
components: [
|
|
364
|
-
{ name: "from", type: "address" },
|
|
365
|
-
{ name: "to", type: "address" },
|
|
366
|
-
{ name: "stable", type: "bool" },
|
|
367
|
-
{ name: "factory", type: "address" }
|
|
368
|
-
]
|
|
369
|
-
},
|
|
370
|
-
{ name: "to", type: "address" },
|
|
371
|
-
{ name: "deadline", type: "uint256" }
|
|
372
|
-
],
|
|
373
|
-
outputs: [{ name: "amounts", type: "uint256[]" }],
|
|
374
|
-
stateMutability: "nonpayable"
|
|
375
|
-
},
|
|
376
|
-
{
|
|
377
|
-
type: "function",
|
|
378
|
-
name: "getAmountsOut",
|
|
379
|
-
inputs: [
|
|
380
|
-
{ name: "amountIn", type: "uint256" },
|
|
381
|
-
{
|
|
382
|
-
name: "routes",
|
|
383
|
-
type: "tuple[]",
|
|
384
|
-
components: [
|
|
385
|
-
{ name: "from", type: "address" },
|
|
386
|
-
{ name: "to", type: "address" },
|
|
387
|
-
{ name: "stable", type: "bool" },
|
|
388
|
-
{ name: "factory", type: "address" }
|
|
389
|
-
]
|
|
390
|
-
}
|
|
391
|
-
],
|
|
392
|
-
outputs: [{ name: "amounts", type: "uint256[]" }],
|
|
393
|
-
stateMutability: "view"
|
|
394
|
-
}
|
|
395
|
-
];
|
|
396
|
-
var AERODROME_DEFAULT_FACTORY = "0x420DD381b31aEf6683db6B902084cB0FFECe40Da";
|
|
397
|
-
|
|
398
|
-
// src/relay.ts
|
|
399
|
-
import WebSocket from "ws";
|
|
400
|
-
var RelayClient = class {
|
|
401
|
-
ws = null;
|
|
402
|
-
config;
|
|
403
|
-
heartbeatTimer = null;
|
|
404
|
-
reconnectTimer = null;
|
|
405
|
-
reconnectAttempts = 0;
|
|
406
|
-
intentionalClose = false;
|
|
407
|
-
authenticated = false;
|
|
408
|
-
lastStatus = null;
|
|
409
|
-
onCommand;
|
|
410
|
-
onConnected;
|
|
411
|
-
onDisconnected;
|
|
412
|
-
constructor(config) {
|
|
413
|
-
this.config = {
|
|
414
|
-
url: config.url,
|
|
415
|
-
agentId: config.agentId,
|
|
416
|
-
token: config.token,
|
|
417
|
-
heartbeatIntervalMs: config.heartbeatIntervalMs ?? 3e4,
|
|
418
|
-
reconnectMaxAttempts: config.reconnectMaxAttempts ?? 50
|
|
419
|
-
};
|
|
420
|
-
this.onCommand = config.onCommand;
|
|
421
|
-
this.onConnected = config.onConnected;
|
|
422
|
-
this.onDisconnected = config.onDisconnected;
|
|
423
|
-
}
|
|
424
|
-
get isConnected() {
|
|
425
|
-
return this.ws?.readyState === WebSocket.OPEN && this.authenticated;
|
|
426
|
-
}
|
|
427
|
-
async connect() {
|
|
428
|
-
return new Promise((resolve2, reject) => {
|
|
429
|
-
try {
|
|
430
|
-
this.intentionalClose = false;
|
|
431
|
-
this.ws = new WebSocket(this.config.url);
|
|
432
|
-
const timeout = setTimeout(() => {
|
|
433
|
-
if (this.ws && this.ws.readyState !== WebSocket.OPEN) {
|
|
434
|
-
this.ws.terminate();
|
|
435
|
-
reject(new Error("Connection timeout (15s)"));
|
|
436
|
-
}
|
|
437
|
-
}, 15e3);
|
|
438
|
-
this.ws.on("open", () => {
|
|
439
|
-
clearTimeout(timeout);
|
|
440
|
-
this.reconnectAttempts = 0;
|
|
441
|
-
this.authenticate();
|
|
442
|
-
});
|
|
443
|
-
this.ws.on("message", (data) => {
|
|
444
|
-
try {
|
|
445
|
-
const msg = JSON.parse(data.toString());
|
|
446
|
-
this.handleMessage(msg, resolve2);
|
|
447
|
-
} catch {
|
|
448
|
-
console.error("[relay] Failed to parse message");
|
|
449
|
-
}
|
|
450
|
-
});
|
|
451
|
-
this.ws.on("close", () => {
|
|
452
|
-
this.cleanup();
|
|
453
|
-
this.onDisconnected?.();
|
|
454
|
-
if (!this.intentionalClose) {
|
|
455
|
-
this.scheduleReconnect();
|
|
456
|
-
}
|
|
457
|
-
});
|
|
458
|
-
this.ws.on("error", (err) => {
|
|
459
|
-
console.error("[relay] WebSocket error:", err.message);
|
|
460
|
-
if (!this.authenticated) {
|
|
461
|
-
clearTimeout(timeout);
|
|
462
|
-
reject(err);
|
|
463
|
-
}
|
|
464
|
-
});
|
|
465
|
-
} catch (err) {
|
|
466
|
-
reject(err);
|
|
467
|
-
}
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
disconnect() {
|
|
471
|
-
this.intentionalClose = true;
|
|
472
|
-
this.cleanup();
|
|
473
|
-
if (this.ws) {
|
|
474
|
-
this.ws.close();
|
|
475
|
-
this.ws = null;
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
authenticate() {
|
|
479
|
-
this.send({
|
|
480
|
-
type: "auth",
|
|
481
|
-
agentId: this.config.agentId,
|
|
482
|
-
token: this.config.token
|
|
483
|
-
});
|
|
484
|
-
}
|
|
485
|
-
handleMessage(msg, onAuth) {
|
|
486
|
-
switch (msg.type) {
|
|
487
|
-
case "auth_success":
|
|
488
|
-
this.authenticated = true;
|
|
489
|
-
this.startHeartbeat();
|
|
490
|
-
this.onConnected?.();
|
|
491
|
-
onAuth?.();
|
|
492
|
-
console.log(`[relay] Authenticated as agent ${this.config.agentId}`);
|
|
493
|
-
break;
|
|
494
|
-
case "auth_error":
|
|
495
|
-
console.error("[relay] Auth failed:", msg.error);
|
|
496
|
-
this.disconnect();
|
|
497
|
-
break;
|
|
498
|
-
case "command":
|
|
499
|
-
this.onCommand?.(msg.command);
|
|
500
|
-
break;
|
|
501
|
-
case "pong":
|
|
502
|
-
break;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
sendCommandAck(commandId, success, message) {
|
|
506
|
-
this.send({
|
|
507
|
-
type: "command_ack",
|
|
508
|
-
agentId: this.config.agentId,
|
|
509
|
-
commandId,
|
|
510
|
-
success,
|
|
511
|
-
message,
|
|
512
|
-
timestamp: Date.now()
|
|
513
|
-
});
|
|
514
|
-
}
|
|
515
|
-
send(msg) {
|
|
516
|
-
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
517
|
-
this.ws.send(JSON.stringify(msg));
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
sendHeartbeat(status) {
|
|
521
|
-
this.lastStatus = status;
|
|
522
|
-
this.send({
|
|
523
|
-
type: "heartbeat",
|
|
524
|
-
agentId: this.config.agentId,
|
|
525
|
-
status,
|
|
526
|
-
timestamp: Date.now()
|
|
527
|
-
});
|
|
528
|
-
}
|
|
529
|
-
sendMessage(messageType, level, title, body, data) {
|
|
530
|
-
this.send({
|
|
531
|
-
type: "message",
|
|
532
|
-
agentId: this.config.agentId,
|
|
533
|
-
messageType,
|
|
534
|
-
level,
|
|
535
|
-
title,
|
|
536
|
-
body,
|
|
537
|
-
data,
|
|
538
|
-
timestamp: Date.now()
|
|
539
|
-
});
|
|
540
|
-
}
|
|
541
|
-
sendTradeSignal(signal) {
|
|
542
|
-
this.send({
|
|
543
|
-
type: "trade_signal",
|
|
544
|
-
agentId: this.config.agentId,
|
|
545
|
-
signal,
|
|
546
|
-
timestamp: Date.now()
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
startHeartbeat() {
|
|
550
|
-
this.stopHeartbeat();
|
|
551
|
-
this.heartbeatTimer = setInterval(() => {
|
|
552
|
-
if (this.lastStatus) {
|
|
553
|
-
this.sendHeartbeat(this.lastStatus);
|
|
554
|
-
}
|
|
555
|
-
}, this.config.heartbeatIntervalMs);
|
|
556
|
-
}
|
|
557
|
-
stopHeartbeat() {
|
|
558
|
-
if (this.heartbeatTimer) {
|
|
559
|
-
clearInterval(this.heartbeatTimer);
|
|
560
|
-
this.heartbeatTimer = null;
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
cleanup() {
|
|
564
|
-
this.stopHeartbeat();
|
|
565
|
-
this.authenticated = false;
|
|
566
|
-
if (this.reconnectTimer) {
|
|
567
|
-
clearTimeout(this.reconnectTimer);
|
|
568
|
-
this.reconnectTimer = null;
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
scheduleReconnect() {
|
|
572
|
-
if (this.reconnectAttempts >= this.config.reconnectMaxAttempts) {
|
|
573
|
-
console.error(`[relay] Max reconnect attempts (${this.config.reconnectMaxAttempts}) reached`);
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
|
|
577
|
-
this.reconnectAttempts++;
|
|
578
|
-
console.log(`[relay] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
579
|
-
this.reconnectTimer = setTimeout(async () => {
|
|
580
|
-
try {
|
|
581
|
-
await this.connect();
|
|
582
|
-
} catch (err) {
|
|
583
|
-
console.error("[relay] Reconnect failed:", err.message);
|
|
584
|
-
}
|
|
585
|
-
}, delay);
|
|
586
|
-
}
|
|
587
|
-
};
|
|
588
|
-
|
|
589
|
-
// src/signal.ts
|
|
590
|
-
var SignalReporter = class {
|
|
591
|
-
relay;
|
|
592
|
-
constructor(relay) {
|
|
593
|
-
this.relay = relay;
|
|
594
|
-
}
|
|
595
|
-
reportTrade(signal) {
|
|
596
|
-
if (!this.relay.isConnected) {
|
|
597
|
-
console.warn("[signal] Not connected to relay \u2014 trade signal queued locally");
|
|
598
|
-
return;
|
|
599
|
-
}
|
|
600
|
-
this.relay.sendTradeSignal(signal);
|
|
601
|
-
this.relay.sendMessage(
|
|
602
|
-
"trade_executed",
|
|
603
|
-
"success",
|
|
604
|
-
`${signal.side.toUpperCase()} ${signal.symbol}`,
|
|
605
|
-
`${signal.side} ${signal.size} ${signal.symbol} @ $${signal.price.toFixed(2)} on ${signal.venue}`,
|
|
606
|
-
{ signal }
|
|
607
|
-
);
|
|
608
|
-
}
|
|
609
|
-
reportPerpFill(signal) {
|
|
610
|
-
if (!this.relay.isConnected) return;
|
|
611
|
-
this.relay.sendTradeSignal(signal);
|
|
612
|
-
this.relay.sendMessage(
|
|
613
|
-
"perp_fill",
|
|
614
|
-
"success",
|
|
615
|
-
`Perp ${signal.side.toUpperCase()} ${signal.symbol}`,
|
|
616
|
-
`${signal.side} ${signal.size} ${signal.symbol} @ $${signal.price.toFixed(2)} (${signal.leverage ?? 1}x)`,
|
|
617
|
-
{ signal }
|
|
618
|
-
);
|
|
619
|
-
}
|
|
620
|
-
reportPredictionFill(signal) {
|
|
621
|
-
if (!this.relay.isConnected) return;
|
|
622
|
-
this.relay.sendTradeSignal(signal);
|
|
623
|
-
this.relay.sendMessage(
|
|
624
|
-
"prediction_fill",
|
|
625
|
-
"success",
|
|
626
|
-
`Prediction ${signal.side.toUpperCase()}`,
|
|
627
|
-
`${signal.side} $${signal.size.toFixed(2)} on ${signal.symbol} @ ${signal.price.toFixed(4)}`,
|
|
628
|
-
{ signal }
|
|
629
|
-
);
|
|
630
|
-
}
|
|
631
|
-
reportSpotFill(signal) {
|
|
632
|
-
if (!this.relay.isConnected) return;
|
|
633
|
-
this.relay.sendTradeSignal(signal);
|
|
634
|
-
this.relay.sendMessage(
|
|
635
|
-
"spot_fill",
|
|
636
|
-
"success",
|
|
637
|
-
`Spot ${signal.side.toUpperCase()} ${signal.symbol}`,
|
|
638
|
-
`${signal.side} ${signal.size} ${signal.symbol} @ $${signal.price.toFixed(4)} on ${signal.venue} (${signal.chain ?? "unknown"})`,
|
|
639
|
-
{ signal }
|
|
640
|
-
);
|
|
641
|
-
}
|
|
642
|
-
reportBridgeFill(signal) {
|
|
643
|
-
if (!this.relay.isConnected) return;
|
|
644
|
-
this.relay.sendTradeSignal(signal);
|
|
645
|
-
this.relay.sendMessage(
|
|
646
|
-
"bridge_fill",
|
|
647
|
-
"success",
|
|
648
|
-
`Bridge ${signal.symbol}`,
|
|
649
|
-
`Bridged ${signal.size} ${signal.symbol} to ${signal.chain ?? "unknown"} via ${signal.venue}`,
|
|
650
|
-
{ signal }
|
|
651
|
-
);
|
|
652
|
-
}
|
|
653
|
-
reportError(title, body, data) {
|
|
654
|
-
if (!this.relay.isConnected) return;
|
|
655
|
-
this.relay.sendMessage("error", "error", title, body, data);
|
|
656
|
-
}
|
|
657
|
-
reportInfo(title, body, data) {
|
|
658
|
-
if (!this.relay.isConnected) return;
|
|
659
|
-
this.relay.sendMessage("info", "info", title, body, data);
|
|
660
|
-
}
|
|
661
|
-
};
|
|
662
|
-
|
|
663
|
-
// src/store.ts
|
|
664
|
-
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync, existsSync as existsSync2 } from "fs";
|
|
665
|
-
import { dirname } from "path";
|
|
666
|
-
var FileStore = class {
|
|
667
|
-
data = {};
|
|
668
|
-
filePath;
|
|
669
|
-
constructor(filePath = "data/strategy-store.json") {
|
|
670
|
-
this.filePath = filePath;
|
|
671
|
-
this.load();
|
|
672
|
-
}
|
|
673
|
-
load() {
|
|
674
|
-
try {
|
|
675
|
-
if (existsSync2(this.filePath)) {
|
|
676
|
-
const raw = readFileSync2(this.filePath, "utf-8");
|
|
677
|
-
this.data = JSON.parse(raw);
|
|
678
|
-
}
|
|
679
|
-
} catch {
|
|
680
|
-
this.data = {};
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
flush() {
|
|
684
|
-
const dir = dirname(this.filePath);
|
|
685
|
-
if (!existsSync2(dir)) {
|
|
686
|
-
mkdirSync(dir, { recursive: true });
|
|
687
|
-
}
|
|
688
|
-
writeFileSync2(this.filePath, JSON.stringify(this.data, null, 2));
|
|
689
|
-
}
|
|
690
|
-
get(key) {
|
|
691
|
-
return this.data[key];
|
|
692
|
-
}
|
|
693
|
-
set(key, value) {
|
|
694
|
-
this.data[key] = value;
|
|
695
|
-
this.flush();
|
|
696
|
-
}
|
|
697
|
-
delete(key) {
|
|
698
|
-
delete this.data[key];
|
|
699
|
-
this.flush();
|
|
700
|
-
}
|
|
701
|
-
keys() {
|
|
702
|
-
return Object.keys(this.data);
|
|
703
|
-
}
|
|
704
|
-
};
|
|
705
|
-
|
|
706
|
-
// src/position-tracker.ts
|
|
707
|
-
var QUOTE_ASSETS = /* @__PURE__ */ new Set(["USDC", "USDbC", "DAI", "USDT", "EURC"]);
|
|
708
|
-
var PERP_VENUES = /* @__PURE__ */ new Set(["hyperliquid_perp"]);
|
|
709
|
-
var PREDICTION_VENUES = /* @__PURE__ */ new Set(["polymarket"]);
|
|
710
|
-
var PositionTracker = class {
|
|
711
|
-
positions = /* @__PURE__ */ new Map();
|
|
712
|
-
trades = [];
|
|
713
|
-
realizedPnL = 0;
|
|
714
|
-
store;
|
|
715
|
-
constructor(store) {
|
|
716
|
-
this.store = store;
|
|
717
|
-
this.load();
|
|
718
|
-
}
|
|
719
|
-
/** Build a unique position key: venue-scoped when venue is provided */
|
|
720
|
-
positionKey(token, venue) {
|
|
721
|
-
return venue ? `${venue}:${token}` : token;
|
|
722
|
-
}
|
|
723
|
-
load() {
|
|
724
|
-
const saved = this.store.get("position_tracker");
|
|
725
|
-
if (saved) {
|
|
726
|
-
this.positions = new Map(saved.positions);
|
|
727
|
-
this.trades = saved.trades;
|
|
728
|
-
this.realizedPnL = saved.realizedPnL;
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
save() {
|
|
732
|
-
this.store.set("position_tracker", {
|
|
733
|
-
positions: Array.from(this.positions.entries()),
|
|
734
|
-
trades: this.trades.slice(-1e3),
|
|
735
|
-
realizedPnL: this.realizedPnL
|
|
736
|
-
});
|
|
737
|
-
}
|
|
738
|
-
/** Returns realized PnL from this trade (non-zero when closing/reducing a position) */
|
|
739
|
-
recordBuy(token, quantity, price, fee, venue, chain, venueFillId) {
|
|
740
|
-
const isPerp = venue ? PERP_VENUES.has(venue) : false;
|
|
741
|
-
const isPrediction = venue ? PREDICTION_VENUES.has(venue) : false;
|
|
742
|
-
if (!isPerp && !isPrediction && QUOTE_ASSETS.has(token)) return;
|
|
743
|
-
const key = this.positionKey(token, venue);
|
|
744
|
-
const existing = this.positions.get(key);
|
|
745
|
-
let tradePnL = 0;
|
|
746
|
-
if (existing) {
|
|
747
|
-
if (isPerp && existing.quantity < 0) {
|
|
748
|
-
const closedQty = Math.min(quantity, Math.abs(existing.quantity));
|
|
749
|
-
tradePnL = (existing.costBasisPerUnit - price) * closedQty - fee;
|
|
750
|
-
this.realizedPnL += tradePnL;
|
|
751
|
-
existing.quantity += quantity;
|
|
752
|
-
if (Math.abs(existing.quantity) <= 1e-6) {
|
|
753
|
-
this.positions.delete(key);
|
|
754
|
-
} else if (existing.quantity > 0) {
|
|
755
|
-
existing.costBasisPerUnit = price;
|
|
756
|
-
}
|
|
757
|
-
} else {
|
|
758
|
-
const totalQty = existing.quantity + quantity;
|
|
759
|
-
const totalCost = existing.costBasisPerUnit * existing.quantity + price * quantity;
|
|
760
|
-
existing.costBasisPerUnit = totalCost / totalQty;
|
|
761
|
-
existing.quantity = totalQty;
|
|
762
|
-
}
|
|
763
|
-
} else {
|
|
764
|
-
this.positions.set(key, {
|
|
765
|
-
token,
|
|
766
|
-
quantity,
|
|
767
|
-
costBasisPerUnit: price,
|
|
768
|
-
entryTimestamp: Date.now(),
|
|
769
|
-
venue,
|
|
770
|
-
chain
|
|
771
|
-
});
|
|
772
|
-
}
|
|
773
|
-
this.trades.push({
|
|
774
|
-
token,
|
|
775
|
-
action: "buy",
|
|
776
|
-
quantity,
|
|
777
|
-
price,
|
|
778
|
-
fee,
|
|
779
|
-
timestamp: Date.now(),
|
|
780
|
-
venue,
|
|
781
|
-
chain,
|
|
782
|
-
venueFillId
|
|
783
|
-
});
|
|
784
|
-
this.save();
|
|
785
|
-
return tradePnL;
|
|
786
|
-
}
|
|
787
|
-
/** Returns realized PnL from this trade (non-zero when closing/reducing a position) */
|
|
788
|
-
recordSell(token, quantity, price, fee, venue, chain, venueFillId) {
|
|
789
|
-
const isPerp = venue ? PERP_VENUES.has(venue) : false;
|
|
790
|
-
const isPrediction = venue ? PREDICTION_VENUES.has(venue) : false;
|
|
791
|
-
if (!isPerp && !isPrediction && QUOTE_ASSETS.has(token)) return;
|
|
792
|
-
const key = this.positionKey(token, venue);
|
|
793
|
-
const existing = this.positions.get(key);
|
|
794
|
-
if (existing) {
|
|
795
|
-
if (isPerp && existing.quantity > 0) {
|
|
796
|
-
const closedQty = Math.min(quantity, existing.quantity);
|
|
797
|
-
const pnl = (price - existing.costBasisPerUnit) * closedQty - fee;
|
|
798
|
-
this.realizedPnL += pnl;
|
|
799
|
-
existing.quantity -= quantity;
|
|
800
|
-
if (Math.abs(existing.quantity) <= 1e-6) {
|
|
801
|
-
this.positions.delete(key);
|
|
802
|
-
} else if (existing.quantity < 0) {
|
|
803
|
-
existing.costBasisPerUnit = price;
|
|
804
|
-
}
|
|
805
|
-
} else if (isPerp && existing.quantity <= 0) {
|
|
806
|
-
const totalQty = Math.abs(existing.quantity) + quantity;
|
|
807
|
-
const totalCost = existing.costBasisPerUnit * Math.abs(existing.quantity) + price * quantity;
|
|
808
|
-
existing.costBasisPerUnit = totalCost / totalQty;
|
|
809
|
-
existing.quantity -= quantity;
|
|
810
|
-
} else {
|
|
811
|
-
const pnl = (price - existing.costBasisPerUnit) * quantity - fee;
|
|
812
|
-
this.realizedPnL += pnl;
|
|
813
|
-
existing.quantity -= quantity;
|
|
814
|
-
if (existing.quantity <= 1e-6) {
|
|
815
|
-
this.positions.delete(key);
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
} else if (isPerp) {
|
|
819
|
-
this.positions.set(key, {
|
|
820
|
-
token,
|
|
821
|
-
quantity: -quantity,
|
|
822
|
-
costBasisPerUnit: price,
|
|
823
|
-
entryTimestamp: Date.now(),
|
|
824
|
-
venue,
|
|
825
|
-
chain
|
|
826
|
-
});
|
|
827
|
-
}
|
|
828
|
-
this.trades.push({
|
|
829
|
-
token,
|
|
830
|
-
action: "sell",
|
|
831
|
-
quantity,
|
|
832
|
-
price,
|
|
833
|
-
fee,
|
|
834
|
-
timestamp: Date.now(),
|
|
835
|
-
venue,
|
|
836
|
-
chain,
|
|
837
|
-
venueFillId
|
|
838
|
-
});
|
|
839
|
-
this.save();
|
|
840
|
-
}
|
|
841
|
-
getSummary(prices) {
|
|
842
|
-
let totalUnrealizedPnL = 0;
|
|
843
|
-
const openPositions = Array.from(this.positions.values());
|
|
844
|
-
for (const pos of openPositions) {
|
|
845
|
-
const currentPrice = prices[pos.token];
|
|
846
|
-
if (currentPrice) {
|
|
847
|
-
if (pos.quantity >= 0) {
|
|
848
|
-
totalUnrealizedPnL += (currentPrice - pos.costBasisPerUnit) * pos.quantity;
|
|
849
|
-
} else {
|
|
850
|
-
totalUnrealizedPnL += (pos.costBasisPerUnit - currentPrice) * Math.abs(pos.quantity);
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
return {
|
|
855
|
-
openPositions,
|
|
856
|
-
totalUnrealizedPnL,
|
|
857
|
-
totalRealizedPnL: this.realizedPnL
|
|
858
|
-
};
|
|
859
|
-
}
|
|
860
|
-
getPositions() {
|
|
861
|
-
return Array.from(this.positions.values());
|
|
862
|
-
}
|
|
863
|
-
getTrades(limit) {
|
|
864
|
-
if (limit) return this.trades.slice(-limit);
|
|
865
|
-
return [...this.trades];
|
|
866
|
-
}
|
|
867
|
-
getRealizedPnL() {
|
|
868
|
-
return this.realizedPnL;
|
|
869
|
-
}
|
|
870
|
-
reset() {
|
|
871
|
-
this.positions.clear();
|
|
872
|
-
this.trades = [];
|
|
873
|
-
this.realizedPnL = 0;
|
|
874
|
-
this.save();
|
|
875
|
-
}
|
|
876
|
-
};
|
|
877
|
-
|
|
878
|
-
// src/llm/base.ts
|
|
879
|
-
var BaseLLMAdapter = class {
|
|
880
|
-
config;
|
|
881
|
-
constructor(config) {
|
|
882
|
-
this.config = config;
|
|
883
|
-
}
|
|
884
|
-
getTemperature() {
|
|
885
|
-
return this.config.temperature ?? 0.7;
|
|
886
|
-
}
|
|
887
|
-
getMaxTokens() {
|
|
888
|
-
return this.config.maxTokens ?? 4096;
|
|
889
|
-
}
|
|
890
|
-
};
|
|
891
|
-
|
|
892
|
-
// src/llm/openai.ts
|
|
893
|
-
var OpenAIAdapter = class extends BaseLLMAdapter {
|
|
894
|
-
endpoint;
|
|
895
|
-
constructor(config) {
|
|
896
|
-
super(config);
|
|
897
|
-
this.endpoint = config.endpoint || "https://api.openai.com/v1";
|
|
898
|
-
}
|
|
899
|
-
async chat(messages) {
|
|
900
|
-
const res = await fetch(`${this.endpoint}/chat/completions`, {
|
|
901
|
-
method: "POST",
|
|
902
|
-
headers: {
|
|
903
|
-
"Content-Type": "application/json",
|
|
904
|
-
Authorization: `Bearer ${this.config.apiKey}`
|
|
905
|
-
},
|
|
906
|
-
body: JSON.stringify({
|
|
907
|
-
model: this.config.model || "gpt-4o",
|
|
908
|
-
messages: messages.map((m) => ({ role: m.role, content: m.content })),
|
|
909
|
-
temperature: this.getTemperature(),
|
|
910
|
-
max_tokens: this.getMaxTokens()
|
|
911
|
-
})
|
|
912
|
-
});
|
|
913
|
-
if (!res.ok) {
|
|
914
|
-
const body = await res.text();
|
|
915
|
-
throw new Error(`OpenAI API error ${res.status}: ${body}`);
|
|
916
|
-
}
|
|
917
|
-
const data = await res.json();
|
|
918
|
-
return {
|
|
919
|
-
content: data.choices[0]?.message?.content || "",
|
|
920
|
-
tokens: data.usage ? { input: data.usage.prompt_tokens, output: data.usage.completion_tokens } : void 0
|
|
921
|
-
};
|
|
922
|
-
}
|
|
923
|
-
getMetadata() {
|
|
924
|
-
return {
|
|
925
|
-
provider: "openai",
|
|
926
|
-
model: this.config.model || "gpt-4o"
|
|
927
|
-
};
|
|
928
|
-
}
|
|
929
|
-
};
|
|
930
|
-
|
|
931
|
-
// src/llm/anthropic.ts
|
|
932
|
-
var AnthropicAdapter = class extends BaseLLMAdapter {
|
|
933
|
-
endpoint;
|
|
934
|
-
constructor(config) {
|
|
935
|
-
super(config);
|
|
936
|
-
this.endpoint = config.endpoint || "https://api.anthropic.com";
|
|
937
|
-
}
|
|
938
|
-
async chat(messages) {
|
|
939
|
-
const systemMessage = messages.find((m) => m.role === "system");
|
|
940
|
-
const nonSystemMessages = messages.filter((m) => m.role !== "system");
|
|
941
|
-
const body = {
|
|
942
|
-
model: this.config.model || "claude-sonnet-4-20250514",
|
|
943
|
-
messages: nonSystemMessages.map((m) => ({ role: m.role, content: m.content })),
|
|
944
|
-
max_tokens: this.getMaxTokens(),
|
|
945
|
-
temperature: this.getTemperature()
|
|
946
|
-
};
|
|
947
|
-
if (systemMessage) {
|
|
948
|
-
body.system = systemMessage.content;
|
|
949
|
-
}
|
|
950
|
-
const res = await fetch(`${this.endpoint}/v1/messages`, {
|
|
951
|
-
method: "POST",
|
|
952
|
-
headers: {
|
|
953
|
-
"Content-Type": "application/json",
|
|
954
|
-
"x-api-key": this.config.apiKey || "",
|
|
955
|
-
"anthropic-version": "2023-06-01"
|
|
956
|
-
},
|
|
957
|
-
body: JSON.stringify(body)
|
|
958
|
-
});
|
|
959
|
-
if (!res.ok) {
|
|
960
|
-
const text = await res.text();
|
|
961
|
-
throw new Error(`Anthropic API error ${res.status}: ${text}`);
|
|
962
|
-
}
|
|
963
|
-
const data = await res.json();
|
|
964
|
-
const textContent = data.content.find((c) => c.type === "text");
|
|
965
|
-
return {
|
|
966
|
-
content: textContent?.text || "",
|
|
967
|
-
tokens: data.usage ? { input: data.usage.input_tokens, output: data.usage.output_tokens } : void 0
|
|
968
|
-
};
|
|
969
|
-
}
|
|
970
|
-
getMetadata() {
|
|
971
|
-
return {
|
|
972
|
-
provider: "anthropic",
|
|
973
|
-
model: this.config.model || "claude-sonnet-4-20250514"
|
|
974
|
-
};
|
|
975
|
-
}
|
|
976
|
-
};
|
|
977
|
-
|
|
978
|
-
// src/llm/google.ts
|
|
979
|
-
var GoogleAdapter = class extends BaseLLMAdapter {
|
|
980
|
-
constructor(config) {
|
|
981
|
-
super(config);
|
|
982
|
-
}
|
|
983
|
-
async chat(messages) {
|
|
984
|
-
const model = this.config.model || "gemini-2.5-flash";
|
|
985
|
-
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${this.config.apiKey}`;
|
|
986
|
-
const systemMessage = messages.find((m) => m.role === "system");
|
|
987
|
-
const nonSystemMessages = messages.filter((m) => m.role !== "system");
|
|
988
|
-
const body = {
|
|
989
|
-
contents: nonSystemMessages.map((m) => ({
|
|
990
|
-
role: m.role === "assistant" ? "model" : "user",
|
|
991
|
-
parts: [{ text: m.content }]
|
|
992
|
-
})),
|
|
993
|
-
generationConfig: {
|
|
994
|
-
temperature: this.getTemperature(),
|
|
995
|
-
maxOutputTokens: this.getMaxTokens()
|
|
996
|
-
}
|
|
997
|
-
};
|
|
998
|
-
if (systemMessage) {
|
|
999
|
-
body.systemInstruction = { parts: [{ text: systemMessage.content }] };
|
|
1000
|
-
}
|
|
1001
|
-
const res = await fetch(url, {
|
|
1002
|
-
method: "POST",
|
|
1003
|
-
headers: { "Content-Type": "application/json" },
|
|
1004
|
-
body: JSON.stringify(body)
|
|
1005
|
-
});
|
|
1006
|
-
if (!res.ok) {
|
|
1007
|
-
const text2 = await res.text();
|
|
1008
|
-
throw new Error(`Google AI error ${res.status}: ${text2}`);
|
|
1009
|
-
}
|
|
1010
|
-
const data = await res.json();
|
|
1011
|
-
const text = data.candidates[0]?.content?.parts[0]?.text || "";
|
|
1012
|
-
return {
|
|
1013
|
-
content: text,
|
|
1014
|
-
tokens: data.usageMetadata ? { input: data.usageMetadata.promptTokenCount, output: data.usageMetadata.candidatesTokenCount } : void 0
|
|
1015
|
-
};
|
|
1016
|
-
}
|
|
1017
|
-
getMetadata() {
|
|
1018
|
-
return {
|
|
1019
|
-
provider: "google",
|
|
1020
|
-
model: this.config.model || "gemini-2.5-flash"
|
|
1021
|
-
};
|
|
1022
|
-
}
|
|
1023
|
-
};
|
|
1024
|
-
|
|
1025
|
-
// src/llm/deepseek.ts
|
|
1026
|
-
var DeepSeekAdapter = class extends BaseLLMAdapter {
|
|
1027
|
-
constructor(config) {
|
|
1028
|
-
super(config);
|
|
1029
|
-
}
|
|
1030
|
-
async chat(messages) {
|
|
1031
|
-
const res = await fetch("https://api.deepseek.com/chat/completions", {
|
|
1032
|
-
method: "POST",
|
|
1033
|
-
headers: {
|
|
1034
|
-
"Content-Type": "application/json",
|
|
1035
|
-
Authorization: `Bearer ${this.config.apiKey}`
|
|
1036
|
-
},
|
|
1037
|
-
body: JSON.stringify({
|
|
1038
|
-
model: this.config.model || "deepseek-chat",
|
|
1039
|
-
messages: messages.map((m) => ({ role: m.role, content: m.content })),
|
|
1040
|
-
temperature: this.getTemperature(),
|
|
1041
|
-
max_tokens: this.getMaxTokens()
|
|
1042
|
-
})
|
|
1043
|
-
});
|
|
1044
|
-
if (!res.ok) {
|
|
1045
|
-
const body = await res.text();
|
|
1046
|
-
throw new Error(`DeepSeek API error ${res.status}: ${body}`);
|
|
1047
|
-
}
|
|
1048
|
-
const data = await res.json();
|
|
1049
|
-
return {
|
|
1050
|
-
content: data.choices[0]?.message?.content || "",
|
|
1051
|
-
tokens: data.usage ? { input: data.usage.prompt_tokens, output: data.usage.completion_tokens } : void 0
|
|
1052
|
-
};
|
|
1053
|
-
}
|
|
1054
|
-
getMetadata() {
|
|
1055
|
-
return {
|
|
1056
|
-
provider: "deepseek",
|
|
1057
|
-
model: this.config.model || "deepseek-chat"
|
|
1058
|
-
};
|
|
1059
|
-
}
|
|
1060
|
-
};
|
|
1061
|
-
|
|
1062
|
-
// src/llm/mistral.ts
|
|
1063
|
-
var MistralAdapter = class extends BaseLLMAdapter {
|
|
1064
|
-
constructor(config) {
|
|
1065
|
-
super(config);
|
|
1066
|
-
}
|
|
1067
|
-
async chat(messages) {
|
|
1068
|
-
const res = await fetch("https://api.mistral.ai/v1/chat/completions", {
|
|
1069
|
-
method: "POST",
|
|
1070
|
-
headers: {
|
|
1071
|
-
"Content-Type": "application/json",
|
|
1072
|
-
Authorization: `Bearer ${this.config.apiKey}`
|
|
1073
|
-
},
|
|
1074
|
-
body: JSON.stringify({
|
|
1075
|
-
model: this.config.model || "mistral-large-latest",
|
|
1076
|
-
messages: messages.map((m) => ({ role: m.role, content: m.content })),
|
|
1077
|
-
temperature: this.getTemperature(),
|
|
1078
|
-
max_tokens: this.getMaxTokens()
|
|
1079
|
-
})
|
|
1080
|
-
});
|
|
1081
|
-
if (!res.ok) {
|
|
1082
|
-
const body = await res.text();
|
|
1083
|
-
throw new Error(`Mistral API error ${res.status}: ${body}`);
|
|
1084
|
-
}
|
|
1085
|
-
const data = await res.json();
|
|
1086
|
-
return {
|
|
1087
|
-
content: data.choices[0]?.message?.content || "",
|
|
1088
|
-
tokens: data.usage ? { input: data.usage.prompt_tokens, output: data.usage.completion_tokens } : void 0
|
|
1089
|
-
};
|
|
1090
|
-
}
|
|
1091
|
-
getMetadata() {
|
|
1092
|
-
return {
|
|
1093
|
-
provider: "mistral",
|
|
1094
|
-
model: this.config.model || "mistral-large-latest"
|
|
1095
|
-
};
|
|
1096
|
-
}
|
|
1097
|
-
};
|
|
1098
|
-
|
|
1099
|
-
// src/llm/groq.ts
|
|
1100
|
-
var GroqAdapter = class extends BaseLLMAdapter {
|
|
1101
|
-
constructor(config) {
|
|
1102
|
-
super(config);
|
|
1103
|
-
}
|
|
1104
|
-
async chat(messages) {
|
|
1105
|
-
const res = await fetch("https://api.groq.com/openai/v1/chat/completions", {
|
|
1106
|
-
method: "POST",
|
|
1107
|
-
headers: {
|
|
1108
|
-
"Content-Type": "application/json",
|
|
1109
|
-
Authorization: `Bearer ${this.config.apiKey}`
|
|
1110
|
-
},
|
|
1111
|
-
body: JSON.stringify({
|
|
1112
|
-
model: this.config.model || "llama-3.3-70b-versatile",
|
|
1113
|
-
messages: messages.map((m) => ({ role: m.role, content: m.content })),
|
|
1114
|
-
temperature: this.getTemperature(),
|
|
1115
|
-
max_tokens: this.getMaxTokens()
|
|
1116
|
-
})
|
|
1117
|
-
});
|
|
1118
|
-
if (!res.ok) {
|
|
1119
|
-
const body = await res.text();
|
|
1120
|
-
throw new Error(`Groq API error ${res.status}: ${body}`);
|
|
1121
|
-
}
|
|
1122
|
-
const data = await res.json();
|
|
1123
|
-
return {
|
|
1124
|
-
content: data.choices[0]?.message?.content || "",
|
|
1125
|
-
tokens: data.usage ? { input: data.usage.prompt_tokens, output: data.usage.completion_tokens } : void 0
|
|
1126
|
-
};
|
|
1127
|
-
}
|
|
1128
|
-
getMetadata() {
|
|
1129
|
-
return {
|
|
1130
|
-
provider: "groq",
|
|
1131
|
-
model: this.config.model || "llama-3.3-70b-versatile"
|
|
1132
|
-
};
|
|
1133
|
-
}
|
|
1134
|
-
};
|
|
1135
|
-
|
|
1136
|
-
// src/llm/together.ts
|
|
1137
|
-
var TogetherAdapter = class extends BaseLLMAdapter {
|
|
1138
|
-
constructor(config) {
|
|
1139
|
-
super(config);
|
|
1140
|
-
}
|
|
1141
|
-
async chat(messages) {
|
|
1142
|
-
const res = await fetch("https://api.together.xyz/v1/chat/completions", {
|
|
1143
|
-
method: "POST",
|
|
1144
|
-
headers: {
|
|
1145
|
-
"Content-Type": "application/json",
|
|
1146
|
-
Authorization: `Bearer ${this.config.apiKey}`
|
|
1147
|
-
},
|
|
1148
|
-
body: JSON.stringify({
|
|
1149
|
-
model: this.config.model || "meta-llama/Llama-3.3-70B-Instruct-Turbo",
|
|
1150
|
-
messages: messages.map((m) => ({ role: m.role, content: m.content })),
|
|
1151
|
-
temperature: this.getTemperature(),
|
|
1152
|
-
max_tokens: this.getMaxTokens()
|
|
1153
|
-
})
|
|
1154
|
-
});
|
|
1155
|
-
if (!res.ok) {
|
|
1156
|
-
const body = await res.text();
|
|
1157
|
-
throw new Error(`Together API error ${res.status}: ${body}`);
|
|
1158
|
-
}
|
|
1159
|
-
const data = await res.json();
|
|
1160
|
-
return {
|
|
1161
|
-
content: data.choices[0]?.message?.content || "",
|
|
1162
|
-
tokens: data.usage ? { input: data.usage.prompt_tokens, output: data.usage.completion_tokens } : void 0
|
|
1163
|
-
};
|
|
1164
|
-
}
|
|
1165
|
-
getMetadata() {
|
|
1166
|
-
return {
|
|
1167
|
-
provider: "together",
|
|
1168
|
-
model: this.config.model || "meta-llama/Llama-3.3-70B-Instruct-Turbo"
|
|
1169
|
-
};
|
|
1170
|
-
}
|
|
1171
|
-
};
|
|
1172
|
-
|
|
1173
|
-
// src/llm/ollama.ts
|
|
1174
|
-
var OllamaAdapter = class extends BaseLLMAdapter {
|
|
1175
|
-
endpoint;
|
|
1176
|
-
constructor(config) {
|
|
1177
|
-
super(config);
|
|
1178
|
-
this.endpoint = config.endpoint || "http://localhost:11434";
|
|
1179
|
-
}
|
|
1180
|
-
async chat(messages) {
|
|
1181
|
-
const res = await fetch(`${this.endpoint}/api/chat`, {
|
|
1182
|
-
method: "POST",
|
|
1183
|
-
headers: { "Content-Type": "application/json" },
|
|
1184
|
-
body: JSON.stringify({
|
|
1185
|
-
model: this.config.model || "llama3.3",
|
|
1186
|
-
messages: messages.map((m) => ({ role: m.role, content: m.content })),
|
|
1187
|
-
stream: false,
|
|
1188
|
-
options: {
|
|
1189
|
-
temperature: this.getTemperature(),
|
|
1190
|
-
num_predict: this.getMaxTokens()
|
|
1191
|
-
}
|
|
1192
|
-
})
|
|
1193
|
-
});
|
|
1194
|
-
if (!res.ok) {
|
|
1195
|
-
const body = await res.text();
|
|
1196
|
-
throw new Error(`Ollama error ${res.status}: ${body}`);
|
|
1197
|
-
}
|
|
1198
|
-
const data = await res.json();
|
|
1199
|
-
return {
|
|
1200
|
-
content: data.message?.content || "",
|
|
1201
|
-
tokens: data.prompt_eval_count ? { input: data.prompt_eval_count, output: data.eval_count || 0 } : void 0
|
|
1202
|
-
};
|
|
1203
|
-
}
|
|
1204
|
-
getMetadata() {
|
|
1205
|
-
return {
|
|
1206
|
-
provider: "ollama",
|
|
1207
|
-
model: this.config.model || "llama3.3"
|
|
1208
|
-
};
|
|
1209
|
-
}
|
|
1210
|
-
};
|
|
1211
|
-
|
|
1212
|
-
// src/llm/index.ts
|
|
1213
|
-
function createLLMAdapter(config) {
|
|
1214
|
-
switch (config.provider) {
|
|
1215
|
-
case "openai":
|
|
1216
|
-
return new OpenAIAdapter(config);
|
|
1217
|
-
case "anthropic":
|
|
1218
|
-
return new AnthropicAdapter(config);
|
|
1219
|
-
case "google":
|
|
1220
|
-
return new GoogleAdapter(config);
|
|
1221
|
-
case "deepseek":
|
|
1222
|
-
return new DeepSeekAdapter(config);
|
|
1223
|
-
case "mistral":
|
|
1224
|
-
return new MistralAdapter(config);
|
|
1225
|
-
case "groq":
|
|
1226
|
-
return new GroqAdapter(config);
|
|
1227
|
-
case "together":
|
|
1228
|
-
return new TogetherAdapter(config);
|
|
1229
|
-
case "ollama":
|
|
1230
|
-
return new OllamaAdapter(config);
|
|
1231
|
-
default:
|
|
1232
|
-
throw new Error(`Unknown LLM provider: ${config.provider}`);
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
// src/strategy/templates.ts
|
|
1237
|
-
var templates = [
|
|
1238
|
-
{
|
|
1239
|
-
id: "momentum",
|
|
1240
|
-
name: "Momentum Trader",
|
|
1241
|
-
description: "Identifies tokens with strong upward price momentum and rides the trend. Uses LLM to analyze price action and market sentiment.",
|
|
1242
|
-
category: "momentum",
|
|
1243
|
-
venues: ["hyperliquid_perp", "hyperliquid_spot"],
|
|
1244
|
-
riskLevel: "moderate",
|
|
1245
|
-
systemPrompt: `You are a momentum trading agent. Analyze the provided market data and identify tokens with strong upward momentum.
|
|
1246
|
-
|
|
1247
|
-
Rules:
|
|
1248
|
-
- Only trade tokens with clear directional momentum (avoid choppy markets)
|
|
1249
|
-
- Use trailing stops to protect gains
|
|
1250
|
-
- Size positions based on conviction (higher confidence = larger position)
|
|
1251
|
-
- Cut losses quickly if momentum reverses
|
|
1252
|
-
- Consider volume as confirmation of momentum
|
|
1253
|
-
|
|
1254
|
-
Return a JSON array of trade signals.`,
|
|
1255
|
-
code: `
|
|
1256
|
-
const prices = context.market.getPrices();
|
|
1257
|
-
const positions = context.position.openPositions;
|
|
1258
|
-
|
|
1259
|
-
const messages = [
|
|
1260
|
-
{ 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.' },
|
|
1261
|
-
{ role: 'user', content: 'Current prices: ' + JSON.stringify(prices) + '\\nOpen positions: ' + JSON.stringify(positions.map(p => p.token)) + '\\nAnalyze for momentum opportunities.' }
|
|
1262
|
-
];
|
|
1263
|
-
|
|
1264
|
-
const response = await context.llm.chat(messages);
|
|
1265
|
-
try {
|
|
1266
|
-
const signals = JSON.parse(response.content);
|
|
1267
|
-
return Array.isArray(signals) ? signals : [];
|
|
1268
|
-
} catch {
|
|
1269
|
-
return [];
|
|
1270
|
-
}
|
|
1271
|
-
`
|
|
1272
|
-
},
|
|
1273
|
-
{
|
|
1274
|
-
id: "value",
|
|
1275
|
-
name: "Value Investor",
|
|
1276
|
-
description: "Identifies undervalued tokens based on fundamental analysis. Buys dips and holds for mean reversion.",
|
|
1277
|
-
category: "value",
|
|
1278
|
-
venues: ["hyperliquid_spot"],
|
|
1279
|
-
riskLevel: "conservative",
|
|
1280
|
-
systemPrompt: `You are a value investing agent. Identify tokens trading below their intrinsic value.
|
|
1281
|
-
|
|
1282
|
-
Rules:
|
|
1283
|
-
- Focus on established tokens with strong fundamentals
|
|
1284
|
-
- Buy on significant dips (>10% from recent highs)
|
|
1285
|
-
- Hold positions longer (swing/position timeframe)
|
|
1286
|
-
- Avoid chasing pumps
|
|
1287
|
-
- Diversify across sectors
|
|
1288
|
-
|
|
1289
|
-
Return a JSON array of trade signals.`,
|
|
1290
|
-
code: `
|
|
1291
|
-
const prices = context.market.getPrices();
|
|
1292
|
-
const positions = context.position.openPositions;
|
|
1293
|
-
|
|
1294
|
-
const messages = [
|
|
1295
|
-
{ 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.' },
|
|
1296
|
-
{ role: 'user', content: 'Current prices: ' + JSON.stringify(prices) + '\\nPositions: ' + JSON.stringify(positions.map(p => ({ token: p.token, entry: p.costBasisPerUnit }))) + '\\nLook for value opportunities.' }
|
|
1297
|
-
];
|
|
1298
|
-
|
|
1299
|
-
const response = await context.llm.chat(messages);
|
|
1300
|
-
try {
|
|
1301
|
-
const signals = JSON.parse(response.content);
|
|
1302
|
-
return Array.isArray(signals) ? signals : [];
|
|
1303
|
-
} catch {
|
|
1304
|
-
return [];
|
|
1305
|
-
}
|
|
1306
|
-
`
|
|
1307
|
-
},
|
|
1308
|
-
{
|
|
1309
|
-
id: "arbitrage",
|
|
1310
|
-
name: "Arbitrage Hunter",
|
|
1311
|
-
description: "Scans for price discrepancies across venues and chains. Executes quickly to capture spreads.",
|
|
1312
|
-
category: "arbitrage",
|
|
1313
|
-
venues: ["hyperliquid_perp", "hyperliquid_spot", "uniswap", "aerodrome"],
|
|
1314
|
-
riskLevel: "aggressive",
|
|
1315
|
-
systemPrompt: `You are an arbitrage agent. Find price discrepancies between venues.
|
|
1316
|
-
|
|
1317
|
-
Rules:
|
|
1318
|
-
- Speed is critical \u2014 execute quickly before spreads close
|
|
1319
|
-
- Account for fees and slippage in profitability calculations
|
|
1320
|
-
- Only trade when net profit > 0.5% after all costs
|
|
1321
|
-
- Monitor cross-chain opportunities (CEX vs DEX spreads)
|
|
1322
|
-
|
|
1323
|
-
Return a JSON array of trade signals.`,
|
|
1324
|
-
code: `
|
|
1325
|
-
const prices = context.market.getPrices();
|
|
1326
|
-
|
|
1327
|
-
const messages = [
|
|
1328
|
-
{ 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.' },
|
|
1329
|
-
{ role: 'user', content: 'Market prices: ' + JSON.stringify(prices) + '\\nScan for cross-venue arbitrage opportunities.' }
|
|
1330
|
-
];
|
|
1331
|
-
|
|
1332
|
-
const response = await context.llm.chat(messages);
|
|
1333
|
-
try {
|
|
1334
|
-
const signals = JSON.parse(response.content);
|
|
1335
|
-
return Array.isArray(signals) ? signals : [];
|
|
1336
|
-
} catch {
|
|
1337
|
-
return [];
|
|
1338
|
-
}
|
|
1339
|
-
`
|
|
1340
|
-
},
|
|
1341
|
-
{
|
|
1342
|
-
id: "hold",
|
|
1343
|
-
name: "Hold (No Trading)",
|
|
1344
|
-
description: "Passive strategy that makes no trades. Useful for monitoring only.",
|
|
1345
|
-
category: "custom",
|
|
1346
|
-
venues: [],
|
|
1347
|
-
riskLevel: "conservative",
|
|
1348
|
-
systemPrompt: "",
|
|
1349
|
-
code: `return [];`
|
|
1350
|
-
}
|
|
1351
|
-
];
|
|
1352
|
-
function getTemplate(id) {
|
|
1353
|
-
return templates.find((t) => t.id === id);
|
|
1354
|
-
}
|
|
1355
|
-
function listTemplates() {
|
|
1356
|
-
return [...templates];
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
// src/strategy/loader.ts
|
|
1360
|
-
import { existsSync as existsSync3 } from "fs";
|
|
1361
|
-
import { resolve } from "path";
|
|
1362
|
-
async function loadStrategy(config) {
|
|
1363
|
-
if (config.file) {
|
|
1364
|
-
return loadFromFile(config.file);
|
|
1365
|
-
}
|
|
1366
|
-
if (config.template) {
|
|
1367
|
-
const template = getTemplate(config.template);
|
|
1368
|
-
if (!template) {
|
|
1369
|
-
throw new Error(`Unknown strategy template: ${config.template}. Available: momentum, value, arbitrage, hold`);
|
|
1370
|
-
}
|
|
1371
|
-
return loadFromCode(template.code);
|
|
1372
|
-
}
|
|
1373
|
-
return holdStrategy;
|
|
1374
|
-
}
|
|
1375
|
-
async function loadFromFile(filePath) {
|
|
1376
|
-
const resolved = resolve(filePath);
|
|
1377
|
-
if (!existsSync3(resolved)) {
|
|
1378
|
-
throw new Error(`Strategy file not found: ${resolved}`);
|
|
1379
|
-
}
|
|
1380
|
-
try {
|
|
1381
|
-
const mod = await import(resolved);
|
|
1382
|
-
const fn = mod.default || mod.strategy;
|
|
1383
|
-
if (typeof fn !== "function") {
|
|
1384
|
-
throw new Error(`Strategy file must export a default function or 'strategy' function`);
|
|
1385
|
-
}
|
|
1386
|
-
return fn;
|
|
1387
|
-
} catch (err) {
|
|
1388
|
-
throw new Error(`Failed to load strategy from ${resolved}: ${err.message}`);
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
async function loadFromCode(code) {
|
|
1392
|
-
const AsyncFunction = Object.getPrototypeOf(async function() {
|
|
1393
|
-
}).constructor;
|
|
1394
|
-
const fn = new AsyncFunction("context", code);
|
|
1395
|
-
return fn;
|
|
1396
|
-
}
|
|
1397
|
-
var holdStrategy = async (_context) => {
|
|
1398
|
-
return [];
|
|
1399
|
-
};
|
|
1400
|
-
|
|
1401
|
-
// src/trading/risk.ts
|
|
1402
|
-
var RiskManager = class {
|
|
1403
|
-
params;
|
|
1404
|
-
dailyPnL = 0;
|
|
1405
|
-
dailyFees = 0;
|
|
1406
|
-
lastResetDate = "";
|
|
1407
|
-
initialCapitalUSD;
|
|
1408
|
-
constructor(params, initialCapitalUSD = 1e4) {
|
|
1409
|
-
this.params = params;
|
|
1410
|
-
this.initialCapitalUSD = initialCapitalUSD;
|
|
1411
|
-
this.resetIfNewDay();
|
|
1412
|
-
}
|
|
1413
|
-
filterSignals(signals, market, openPositionCount) {
|
|
1414
|
-
this.resetIfNewDay();
|
|
1415
|
-
if (this.isDailyLossLimitHit()) {
|
|
1416
|
-
console.log("[risk] Daily loss limit hit \u2014 blocking all trades");
|
|
1417
|
-
return [];
|
|
1418
|
-
}
|
|
1419
|
-
return signals.filter((signal) => this.validateSignal(signal, market, openPositionCount));
|
|
1420
|
-
}
|
|
1421
|
-
validateSignal(signal, market, openPositionCount) {
|
|
1422
|
-
const threshold = this.params.confidenceThreshold ?? 0.5;
|
|
1423
|
-
if (signal.confidence !== void 0 && signal.confidence < threshold) {
|
|
1424
|
-
console.log(`[risk] Blocked ${signal.symbol}: confidence ${signal.confidence} < ${threshold}`);
|
|
1425
|
-
return false;
|
|
1426
|
-
}
|
|
1427
|
-
const tradeValue = signal.size * signal.price;
|
|
1428
|
-
const maxPositionValue = this.params.maxPositionSizeBps / 1e4 * this.initialCapitalUSD;
|
|
1429
|
-
if (tradeValue > maxPositionValue) {
|
|
1430
|
-
console.log(`[risk] Blocked ${signal.symbol}: trade $${tradeValue.toFixed(0)} > max $${maxPositionValue.toFixed(0)}`);
|
|
1431
|
-
return false;
|
|
1432
|
-
}
|
|
1433
|
-
if (tradeValue < this.params.minTradeValueUSD) {
|
|
1434
|
-
console.log(`[risk] Blocked ${signal.symbol}: trade $${tradeValue.toFixed(2)} < min $${this.params.minTradeValueUSD}`);
|
|
1435
|
-
return false;
|
|
1436
|
-
}
|
|
1437
|
-
if ((signal.side === "buy" || signal.side === "long") && openPositionCount >= this.params.maxConcurrentPositions) {
|
|
1438
|
-
console.log(`[risk] Blocked ${signal.symbol}: ${openPositionCount} positions >= max ${this.params.maxConcurrentPositions}`);
|
|
1439
|
-
return false;
|
|
1440
|
-
}
|
|
1441
|
-
if (signal.orderType === "market") {
|
|
1442
|
-
const currentPrice = market.getPrice(signal.symbol);
|
|
1443
|
-
if (currentPrice) {
|
|
1444
|
-
const slippageBps = Math.abs(signal.price - currentPrice) / currentPrice * 1e4;
|
|
1445
|
-
if (slippageBps > this.params.maxSlippageBps) {
|
|
1446
|
-
console.log(`[risk] Blocked ${signal.symbol}: slippage ${slippageBps.toFixed(0)}bps > max ${this.params.maxSlippageBps}bps`);
|
|
1447
|
-
return false;
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
return true;
|
|
1452
|
-
}
|
|
1453
|
-
updateParams(updates) {
|
|
1454
|
-
if (updates.maxPositionSizeBps !== void 0) this.params.maxPositionSizeBps = updates.maxPositionSizeBps;
|
|
1455
|
-
if (updates.maxDailyLossBps !== void 0) this.params.maxDailyLossBps = updates.maxDailyLossBps;
|
|
1456
|
-
if (updates.maxConcurrentPositions !== void 0) this.params.maxConcurrentPositions = updates.maxConcurrentPositions;
|
|
1457
|
-
if (updates.maxSlippageBps !== void 0) this.params.maxSlippageBps = updates.maxSlippageBps;
|
|
1458
|
-
if (updates.minTradeValueUSD !== void 0) this.params.minTradeValueUSD = updates.minTradeValueUSD;
|
|
1459
|
-
console.log("[risk] Params updated:", JSON.stringify(this.params));
|
|
1460
|
-
}
|
|
1461
|
-
recordTrade(pnl, fee) {
|
|
1462
|
-
this.dailyPnL += pnl;
|
|
1463
|
-
this.dailyFees += fee;
|
|
1464
|
-
}
|
|
1465
|
-
isDailyLossLimitHit() {
|
|
1466
|
-
const limit = this.params.maxDailyLossBps / 1e4 * this.initialCapitalUSD;
|
|
1467
|
-
return this.dailyPnL < -limit;
|
|
1468
|
-
}
|
|
1469
|
-
getDailyPnL() {
|
|
1470
|
-
return this.dailyPnL;
|
|
1471
|
-
}
|
|
1472
|
-
getDailyLossLimit() {
|
|
1473
|
-
return this.params.maxDailyLossBps / 1e4 * this.initialCapitalUSD;
|
|
1474
|
-
}
|
|
1475
|
-
resetIfNewDay() {
|
|
1476
|
-
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1477
|
-
if (today !== this.lastResetDate) {
|
|
1478
|
-
this.dailyPnL = 0;
|
|
1479
|
-
this.dailyFees = 0;
|
|
1480
|
-
this.lastResetDate = today;
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
|
-
};
|
|
1484
|
-
|
|
1485
|
-
// src/trading/market.ts
|
|
1486
|
-
var MarketDataService = class {
|
|
1487
|
-
prices = {};
|
|
1488
|
-
lastFetch = 0;
|
|
1489
|
-
cacheTTL;
|
|
1490
|
-
apiUrl;
|
|
1491
|
-
ohlcvCache = /* @__PURE__ */ new Map();
|
|
1492
|
-
constructor(cacheTTLMs = 3e4, apiUrl) {
|
|
1493
|
-
this.cacheTTL = cacheTTLMs;
|
|
1494
|
-
this.apiUrl = apiUrl || null;
|
|
1495
|
-
}
|
|
1496
|
-
async refreshPrices(symbols) {
|
|
1497
|
-
if (symbols.length === 0) return this.prices;
|
|
1498
|
-
const now = Date.now();
|
|
1499
|
-
if (now - this.lastFetch < this.cacheTTL && Object.keys(this.prices).length > 0) {
|
|
1500
|
-
return this.prices;
|
|
1501
|
-
}
|
|
1502
|
-
if (this.apiUrl) {
|
|
1503
|
-
try {
|
|
1504
|
-
const res = await fetch(`${this.apiUrl}/v1/prices`);
|
|
1505
|
-
if (res.ok) {
|
|
1506
|
-
const data = await res.json();
|
|
1507
|
-
for (const [symbol, price] of Object.entries(data)) {
|
|
1508
|
-
this.prices[symbol.toUpperCase()] = price;
|
|
1509
|
-
}
|
|
1510
|
-
this.lastFetch = now;
|
|
1511
|
-
return this.prices;
|
|
1512
|
-
}
|
|
1513
|
-
} catch {
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
|
-
try {
|
|
1517
|
-
const ids = symbols.map((s) => s.toLowerCase()).join(",");
|
|
1518
|
-
const res = await fetch(
|
|
1519
|
-
`https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd`
|
|
1520
|
-
);
|
|
1521
|
-
if (res.ok) {
|
|
1522
|
-
const data = await res.json();
|
|
1523
|
-
for (const [id, price] of Object.entries(data)) {
|
|
1524
|
-
this.prices[id.toUpperCase()] = price.usd;
|
|
1525
|
-
}
|
|
1526
|
-
this.lastFetch = now;
|
|
1527
|
-
}
|
|
1528
|
-
} catch (err) {
|
|
1529
|
-
console.warn("[market] Price fetch failed:", err.message);
|
|
1530
|
-
}
|
|
1531
|
-
return this.prices;
|
|
1532
|
-
}
|
|
1533
|
-
getPrice(symbol) {
|
|
1534
|
-
return this.prices[symbol.toUpperCase()];
|
|
1535
|
-
}
|
|
1536
|
-
getPrices() {
|
|
1537
|
-
return { ...this.prices };
|
|
1538
|
-
}
|
|
1539
|
-
setPrice(symbol, price) {
|
|
1540
|
-
this.prices[symbol.toUpperCase()] = price;
|
|
1541
|
-
}
|
|
1542
|
-
setPrices(prices) {
|
|
1543
|
-
for (const [symbol, price] of Object.entries(prices)) {
|
|
1544
|
-
this.prices[symbol.toUpperCase()] = price;
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1547
|
-
getOHLCV(symbol, timeframe) {
|
|
1548
|
-
const cacheKey = `${symbol.toUpperCase()}:${timeframe}`;
|
|
1549
|
-
const cached = this.ohlcvCache.get(cacheKey);
|
|
1550
|
-
if (cached && Date.now() - cached.fetchedAt < this.cacheTTL) {
|
|
1551
|
-
return cached.data;
|
|
1552
|
-
}
|
|
1553
|
-
this.fetchOHLCV(symbol, timeframe).catch(() => {
|
|
1554
|
-
});
|
|
1555
|
-
return cached?.data || [];
|
|
1556
|
-
}
|
|
1557
|
-
async fetchOHLCV(symbol, timeframe) {
|
|
1558
|
-
const cacheKey = `${symbol.toUpperCase()}:${timeframe}`;
|
|
1559
|
-
if (this.apiUrl) {
|
|
1560
|
-
try {
|
|
1561
|
-
const res = await fetch(
|
|
1562
|
-
`${this.apiUrl}/v1/prices/${symbol.toUpperCase()}/ohlcv?interval=${timeframe}&limit=100`
|
|
1563
|
-
);
|
|
1564
|
-
if (res.ok) {
|
|
1565
|
-
const bars = await res.json();
|
|
1566
|
-
const ohlcv = bars.map((b) => ({
|
|
1567
|
-
timestamp: b.t,
|
|
1568
|
-
open: b.o,
|
|
1569
|
-
high: b.h,
|
|
1570
|
-
low: b.l,
|
|
1571
|
-
close: b.c,
|
|
1572
|
-
volume: b.v
|
|
1573
|
-
}));
|
|
1574
|
-
this.ohlcvCache.set(cacheKey, { data: ohlcv, fetchedAt: Date.now() });
|
|
1575
|
-
return;
|
|
1576
|
-
}
|
|
1577
|
-
} catch {
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
|
-
}
|
|
1581
|
-
};
|
|
1582
|
-
|
|
1583
|
-
// src/paper/executor.ts
|
|
1584
|
-
import { randomUUID } from "crypto";
|
|
1585
|
-
var PaperExecutor = class {
|
|
1586
|
-
cash;
|
|
1587
|
-
positions = /* @__PURE__ */ new Map();
|
|
1588
|
-
trades = [];
|
|
1589
|
-
equityCurve = [];
|
|
1590
|
-
slippageBps;
|
|
1591
|
-
feeRate;
|
|
1592
|
-
peakEquity;
|
|
1593
|
-
maxDrawdown = 0;
|
|
1594
|
-
constructor(initialCash = 1e4, slippageBps = 50, feeRate = 2e-3) {
|
|
1595
|
-
this.cash = initialCash;
|
|
1596
|
-
this.slippageBps = slippageBps;
|
|
1597
|
-
this.feeRate = feeRate;
|
|
1598
|
-
this.peakEquity = initialCash;
|
|
1599
|
-
this.recordEquity();
|
|
1600
|
-
}
|
|
1601
|
-
execute(signal, market) {
|
|
1602
|
-
const marketPrice = market.getPrice(signal.symbol);
|
|
1603
|
-
if (!marketPrice) {
|
|
1604
|
-
console.warn(`[paper] No price for ${signal.symbol}`);
|
|
1605
|
-
return null;
|
|
1606
|
-
}
|
|
1607
|
-
const isBuy = signal.side === "buy" || signal.side === "long";
|
|
1608
|
-
const slippage = this.slippageBps / 1e4 * marketPrice;
|
|
1609
|
-
const executedPrice = isBuy ? marketPrice + slippage : marketPrice - slippage;
|
|
1610
|
-
const fee = signal.size * executedPrice * this.feeRate;
|
|
1611
|
-
if (isBuy) {
|
|
1612
|
-
const totalCost = signal.size * executedPrice + fee;
|
|
1613
|
-
if (totalCost > this.cash) {
|
|
1614
|
-
console.warn(`[paper] Insufficient funds: need $${totalCost.toFixed(2)}, have $${this.cash.toFixed(2)}`);
|
|
1615
|
-
return null;
|
|
1616
|
-
}
|
|
1617
|
-
this.cash -= totalCost;
|
|
1618
|
-
const existing = this.positions.get(signal.symbol);
|
|
1619
|
-
if (existing) {
|
|
1620
|
-
const totalQty = existing.quantity + signal.size;
|
|
1621
|
-
existing.avgPrice = (existing.avgPrice * existing.quantity + executedPrice * signal.size) / totalQty;
|
|
1622
|
-
existing.quantity = totalQty;
|
|
1623
|
-
} else {
|
|
1624
|
-
this.positions.set(signal.symbol, { quantity: signal.size, avgPrice: executedPrice });
|
|
1625
|
-
}
|
|
1626
|
-
} else {
|
|
1627
|
-
const existing = this.positions.get(signal.symbol);
|
|
1628
|
-
if (!existing || existing.quantity < signal.size) {
|
|
1629
|
-
console.warn(`[paper] No position to sell: ${signal.symbol}`);
|
|
1630
|
-
return null;
|
|
1631
|
-
}
|
|
1632
|
-
const proceeds = signal.size * executedPrice - fee;
|
|
1633
|
-
this.cash += proceeds;
|
|
1634
|
-
existing.quantity -= signal.size;
|
|
1635
|
-
if (existing.quantity <= 1e-6) {
|
|
1636
|
-
this.positions.delete(signal.symbol);
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
const trade = {
|
|
1640
|
-
id: randomUUID(),
|
|
1641
|
-
symbol: signal.symbol,
|
|
1642
|
-
side: isBuy ? "buy" : "sell",
|
|
1643
|
-
size: signal.size,
|
|
1644
|
-
entryPrice: executedPrice,
|
|
1645
|
-
fee,
|
|
1646
|
-
timestamp: Date.now(),
|
|
1647
|
-
venue: `paper_${signal.venue}`
|
|
1648
|
-
};
|
|
1649
|
-
this.trades.push(trade);
|
|
1650
|
-
this.recordEquity();
|
|
1651
|
-
return trade;
|
|
1652
|
-
}
|
|
1653
|
-
recordEquity() {
|
|
1654
|
-
const equity = this.getEquity();
|
|
1655
|
-
this.equityCurve.push({ timestamp: Date.now(), equity });
|
|
1656
|
-
if (equity > this.peakEquity) this.peakEquity = equity;
|
|
1657
|
-
const drawdown = (this.peakEquity - equity) / this.peakEquity;
|
|
1658
|
-
if (drawdown > this.maxDrawdown) this.maxDrawdown = drawdown;
|
|
1659
|
-
}
|
|
1660
|
-
getEquity() {
|
|
1661
|
-
let positionValue = 0;
|
|
1662
|
-
for (const [, pos] of this.positions) {
|
|
1663
|
-
positionValue += pos.quantity * pos.avgPrice;
|
|
1664
|
-
}
|
|
1665
|
-
return this.cash + positionValue;
|
|
1666
|
-
}
|
|
1667
|
-
getCash() {
|
|
1668
|
-
return this.cash;
|
|
1669
|
-
}
|
|
1670
|
-
getPositions() {
|
|
1671
|
-
return new Map(this.positions);
|
|
1672
|
-
}
|
|
1673
|
-
getTrades() {
|
|
1674
|
-
return [...this.trades];
|
|
1675
|
-
}
|
|
1676
|
-
getEquityCurve() {
|
|
1677
|
-
return [...this.equityCurve];
|
|
1678
|
-
}
|
|
1679
|
-
getMetrics() {
|
|
1680
|
-
const initialEquity = this.equityCurve[0]?.equity || 0;
|
|
1681
|
-
const currentEquity = this.getEquity();
|
|
1682
|
-
const totalReturn = initialEquity > 0 ? (currentEquity - initialEquity) / initialEquity : 0;
|
|
1683
|
-
const dailyReturns = this.calculateDailyReturns();
|
|
1684
|
-
const sharpeRatio = this.calculateSharpe(dailyReturns);
|
|
1685
|
-
let wins = 0;
|
|
1686
|
-
let losses = 0;
|
|
1687
|
-
let grossProfit = 0;
|
|
1688
|
-
let grossLoss = 0;
|
|
1689
|
-
const sellTrades = this.trades.filter((t) => t.side === "sell");
|
|
1690
|
-
for (const sell of sellTrades) {
|
|
1691
|
-
const buyTrades = this.trades.filter((t) => t.side === "buy" && t.symbol === sell.symbol);
|
|
1692
|
-
const avgEntry = buyTrades.reduce((sum, t) => sum + t.entryPrice, 0) / (buyTrades.length || 1);
|
|
1693
|
-
const pnl = (sell.entryPrice - avgEntry) * sell.size - sell.fee;
|
|
1694
|
-
if (pnl > 0) {
|
|
1695
|
-
wins++;
|
|
1696
|
-
grossProfit += pnl;
|
|
1697
|
-
} else {
|
|
1698
|
-
losses++;
|
|
1699
|
-
grossLoss += Math.abs(pnl);
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
|
-
return {
|
|
1703
|
-
totalReturn,
|
|
1704
|
-
sharpeRatio,
|
|
1705
|
-
maxDrawdown: this.maxDrawdown,
|
|
1706
|
-
winRate: wins + losses > 0 ? wins / (wins + losses) : 0,
|
|
1707
|
-
profitFactor: grossLoss > 0 ? grossProfit / grossLoss : grossProfit > 0 ? Infinity : 0,
|
|
1708
|
-
totalTrades: this.trades.length
|
|
1709
|
-
};
|
|
1710
|
-
}
|
|
1711
|
-
calculateDailyReturns() {
|
|
1712
|
-
if (this.equityCurve.length < 2) return [];
|
|
1713
|
-
const dailyEquity = /* @__PURE__ */ new Map();
|
|
1714
|
-
for (const point of this.equityCurve) {
|
|
1715
|
-
const date = new Date(point.timestamp).toISOString().slice(0, 10);
|
|
1716
|
-
dailyEquity.set(date, point.equity);
|
|
1717
|
-
}
|
|
1718
|
-
const dates = Array.from(dailyEquity.keys()).sort();
|
|
1719
|
-
const returns = [];
|
|
1720
|
-
for (let i = 1; i < dates.length; i++) {
|
|
1721
|
-
const prev = dailyEquity.get(dates[i - 1]);
|
|
1722
|
-
const curr = dailyEquity.get(dates[i]);
|
|
1723
|
-
if (prev > 0) returns.push((curr - prev) / prev);
|
|
1724
|
-
}
|
|
1725
|
-
return returns;
|
|
1726
|
-
}
|
|
1727
|
-
calculateSharpe(returns) {
|
|
1728
|
-
if (returns.length < 2) return 0;
|
|
1729
|
-
const mean = returns.reduce((a, b) => a + b, 0) / returns.length;
|
|
1730
|
-
const variance = returns.reduce((sum, r) => sum + (r - mean) ** 2, 0) / (returns.length - 1);
|
|
1731
|
-
const std = Math.sqrt(variance);
|
|
1732
|
-
if (std === 0) return 0;
|
|
1733
|
-
return mean / std * Math.sqrt(365);
|
|
1734
|
-
}
|
|
1735
|
-
};
|
|
1736
|
-
|
|
1737
|
-
// src/perp/client.ts
|
|
1738
|
-
var HyperliquidClient = class {
|
|
1739
|
-
apiUrl;
|
|
1740
|
-
meta = null;
|
|
1741
|
-
assetIndexCache = /* @__PURE__ */ new Map();
|
|
1742
|
-
constructor(config) {
|
|
1743
|
-
this.apiUrl = config.apiUrl;
|
|
1744
|
-
}
|
|
1745
|
-
// ── INFO API (read-only) ───────────────────────────────────
|
|
1746
|
-
async getMeta() {
|
|
1747
|
-
if (this.meta) return this.meta;
|
|
1748
|
-
const resp = await this.infoRequest({ type: "meta" });
|
|
1749
|
-
this.meta = resp.universe;
|
|
1750
|
-
this.meta.forEach((asset, idx) => {
|
|
1751
|
-
this.assetIndexCache.set(asset.name, idx);
|
|
1752
|
-
});
|
|
1753
|
-
return this.meta;
|
|
1754
|
-
}
|
|
1755
|
-
async getAssetIndex(coin) {
|
|
1756
|
-
if (this.assetIndexCache.has(coin)) return this.assetIndexCache.get(coin);
|
|
1757
|
-
await this.getMeta();
|
|
1758
|
-
const idx = this.assetIndexCache.get(coin);
|
|
1759
|
-
if (idx === void 0) throw new Error(`Unknown instrument: ${coin}`);
|
|
1760
|
-
return idx;
|
|
1761
|
-
}
|
|
1762
|
-
async getAllMids() {
|
|
1763
|
-
return this.infoRequest({ type: "allMids" });
|
|
1764
|
-
}
|
|
1765
|
-
async getClearinghouseState(user) {
|
|
1766
|
-
return this.infoRequest({ type: "clearinghouseState", user });
|
|
1767
|
-
}
|
|
1768
|
-
async getUserFills(user, startTime) {
|
|
1769
|
-
return this.infoRequest({
|
|
1770
|
-
type: "userFills",
|
|
1771
|
-
user,
|
|
1772
|
-
...startTime !== void 0 && { startTime }
|
|
1773
|
-
});
|
|
1774
|
-
}
|
|
1775
|
-
async getUserFillsByTime(user, startTime, endTime) {
|
|
1776
|
-
return this.infoRequest({
|
|
1777
|
-
type: "userFillsByTime",
|
|
1778
|
-
user,
|
|
1779
|
-
startTime,
|
|
1780
|
-
...endTime !== void 0 && { endTime }
|
|
1781
|
-
});
|
|
1782
|
-
}
|
|
1783
|
-
async getOpenOrders(user) {
|
|
1784
|
-
return this.infoRequest({ type: "openOrders", user });
|
|
1785
|
-
}
|
|
1786
|
-
async getL2Book(coin, depth) {
|
|
1787
|
-
return this.infoRequest({
|
|
1788
|
-
type: "l2Book",
|
|
1789
|
-
coin,
|
|
1790
|
-
...depth !== void 0 && { nSigFigs: depth }
|
|
1791
|
-
});
|
|
1792
|
-
}
|
|
1793
|
-
// ── HIGH-LEVEL HELPERS ─────────────────────────────────────
|
|
1794
|
-
async getPositions(user) {
|
|
1795
|
-
const state = await this.getClearinghouseState(user);
|
|
1796
|
-
return state.assetPositions.filter((p) => parseFloat(p.position.szi) !== 0).map((p) => this.parsePosition(p));
|
|
1797
|
-
}
|
|
1798
|
-
async getAccountSummary(user) {
|
|
1799
|
-
const state = await this.getClearinghouseState(user);
|
|
1800
|
-
const cms = state.crossMarginSummary;
|
|
1801
|
-
const totalEquity = parseFloat(cms.accountValue);
|
|
1802
|
-
const totalNotional = parseFloat(cms.totalNtlPos);
|
|
1803
|
-
const totalMarginUsed = parseFloat(cms.totalMarginUsed);
|
|
1804
|
-
return {
|
|
1805
|
-
totalEquity,
|
|
1806
|
-
availableMargin: totalEquity - totalMarginUsed,
|
|
1807
|
-
totalMarginUsed,
|
|
1808
|
-
totalUnrealizedPnl: parseFloat(cms.totalRawUsd) - totalEquity,
|
|
1809
|
-
totalNotional,
|
|
1810
|
-
maintenanceMargin: totalMarginUsed * 0.5,
|
|
1811
|
-
effectiveLeverage: totalEquity > 0 ? totalNotional / totalEquity : 0,
|
|
1812
|
-
cashBalance: parseFloat(cms.accountValue) - state.assetPositions.reduce(
|
|
1813
|
-
(sum, p) => sum + parseFloat(p.position.unrealizedPnl),
|
|
1814
|
-
0
|
|
1815
|
-
)
|
|
1816
|
-
};
|
|
1817
|
-
}
|
|
1818
|
-
async getMarketData(instruments) {
|
|
1819
|
-
const mids = await this.getAllMids();
|
|
1820
|
-
return instruments.filter((inst) => mids[inst] !== void 0).map((inst) => ({
|
|
1821
|
-
instrument: inst,
|
|
1822
|
-
midPrice: parseFloat(mids[inst]),
|
|
1823
|
-
bestBid: parseFloat(mids[inst]),
|
|
1824
|
-
bestAsk: parseFloat(mids[inst]),
|
|
1825
|
-
funding8h: 0,
|
|
1826
|
-
openInterest: 0,
|
|
1827
|
-
volume24h: 0,
|
|
1828
|
-
priceChange24h: 0
|
|
1829
|
-
}));
|
|
1830
|
-
}
|
|
1831
|
-
parseFill(fill) {
|
|
1832
|
-
return {
|
|
1833
|
-
oid: fill.oid,
|
|
1834
|
-
coin: fill.coin,
|
|
1835
|
-
side: fill.side,
|
|
1836
|
-
px: fill.px,
|
|
1837
|
-
sz: fill.sz,
|
|
1838
|
-
fee: fill.fee,
|
|
1839
|
-
time: fill.time,
|
|
1840
|
-
hash: fill.hash,
|
|
1841
|
-
isMaker: fill.startPosition !== fill.px,
|
|
1842
|
-
builderFee: fill.builderFee,
|
|
1843
|
-
liquidation: fill.liquidation
|
|
1844
|
-
};
|
|
1845
|
-
}
|
|
1846
|
-
// ── PRIVATE ────────────────────────────────────────────────
|
|
1847
|
-
parsePosition(ap) {
|
|
1848
|
-
const pos = ap.position;
|
|
1849
|
-
const size = parseFloat(pos.szi);
|
|
1850
|
-
const entryPrice = parseFloat(pos.entryPx || "0");
|
|
1851
|
-
const markPrice = parseFloat(pos.positionValue || "0") / Math.abs(size || 1);
|
|
1852
|
-
return {
|
|
1853
|
-
instrument: pos.coin,
|
|
1854
|
-
assetIndex: this.assetIndexCache.get(pos.coin) ?? -1,
|
|
1855
|
-
size,
|
|
1856
|
-
entryPrice,
|
|
1857
|
-
markPrice,
|
|
1858
|
-
unrealizedPnl: parseFloat(pos.unrealizedPnl),
|
|
1859
|
-
leverage: parseFloat(pos.leverage?.value || "1"),
|
|
1860
|
-
marginType: pos.leverage?.type === "isolated" ? "isolated" : "cross",
|
|
1861
|
-
liquidationPrice: parseFloat(pos.liquidationPx || "0"),
|
|
1862
|
-
notionalUSD: Math.abs(size) * markPrice,
|
|
1863
|
-
marginUsed: parseFloat(pos.marginUsed)
|
|
1864
|
-
};
|
|
1865
|
-
}
|
|
1866
|
-
async infoRequest(body) {
|
|
1867
|
-
const resp = await fetch(`${this.apiUrl}/info`, {
|
|
1868
|
-
method: "POST",
|
|
1869
|
-
headers: { "Content-Type": "application/json" },
|
|
1870
|
-
body: JSON.stringify(body)
|
|
1871
|
-
});
|
|
1872
|
-
if (!resp.ok) {
|
|
1873
|
-
throw new Error(`Hyperliquid Info API error: ${resp.status} ${await resp.text()}`);
|
|
1874
|
-
}
|
|
1875
|
-
return resp.json();
|
|
1876
|
-
}
|
|
1877
|
-
};
|
|
1878
|
-
|
|
1879
|
-
// src/perp/signer.ts
|
|
1880
|
-
import { keccak256 } from "viem";
|
|
1881
|
-
|
|
1882
|
-
// src/perp/msgpack.ts
|
|
1883
|
-
var FOUR_BITS = 16;
|
|
1884
|
-
var FIVE_BITS = 32;
|
|
1885
|
-
var SEVEN_BITS = 128;
|
|
1886
|
-
var EIGHT_BITS = 256;
|
|
1887
|
-
var FIFTEEN_BITS = 32768;
|
|
1888
|
-
var SIXTEEN_BITS = 65536;
|
|
1889
|
-
var THIRTY_ONE_BITS = 2147483648;
|
|
1890
|
-
var THIRTY_TWO_BITS = 4294967296;
|
|
1891
|
-
var SIXTY_THREE_BITS = 9223372036854775808n;
|
|
1892
|
-
var SIXTY_FOUR_BITS = 18446744073709551616n;
|
|
1893
|
-
var textEncoder = new TextEncoder();
|
|
1894
|
-
function encodeMsgpack(object) {
|
|
1895
|
-
const byteParts = [];
|
|
1896
|
-
encodeSlice(object, byteParts);
|
|
1897
|
-
return concatBytes(...byteParts);
|
|
1898
|
-
}
|
|
1899
|
-
function concatBytes(...arrays) {
|
|
1900
|
-
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
|
|
1901
|
-
const result = new Uint8Array(totalLength);
|
|
1902
|
-
let offset = 0;
|
|
1903
|
-
for (const arr of arrays) {
|
|
1904
|
-
result.set(arr, offset);
|
|
1905
|
-
offset += arr.length;
|
|
1906
|
-
}
|
|
1907
|
-
return result;
|
|
1908
|
-
}
|
|
1909
|
-
function encodeFloat64(num) {
|
|
1910
|
-
const dataView = new DataView(new ArrayBuffer(9));
|
|
1911
|
-
dataView.setFloat64(1, num);
|
|
1912
|
-
dataView.setUint8(0, 203);
|
|
1913
|
-
return new Uint8Array(dataView.buffer);
|
|
1914
|
-
}
|
|
1915
|
-
function encodeNumber(num) {
|
|
1916
|
-
if (!Number.isInteger(num)) return encodeFloat64(num);
|
|
1917
|
-
if (num < 0) {
|
|
1918
|
-
if (num >= -FIVE_BITS) return new Uint8Array([num & 255]);
|
|
1919
|
-
if (num >= -SEVEN_BITS) return new Uint8Array([208, num & 255]);
|
|
1920
|
-
if (num >= -FIFTEEN_BITS) {
|
|
1921
|
-
const dv = new DataView(new ArrayBuffer(3));
|
|
1922
|
-
dv.setInt16(1, num);
|
|
1923
|
-
dv.setUint8(0, 209);
|
|
1924
|
-
return new Uint8Array(dv.buffer);
|
|
1925
|
-
}
|
|
1926
|
-
if (num >= -THIRTY_ONE_BITS) {
|
|
1927
|
-
const dv = new DataView(new ArrayBuffer(5));
|
|
1928
|
-
dv.setInt32(1, num);
|
|
1929
|
-
dv.setUint8(0, 210);
|
|
1930
|
-
return new Uint8Array(dv.buffer);
|
|
1931
|
-
}
|
|
1932
|
-
return encodeFloat64(num);
|
|
1933
|
-
}
|
|
1934
|
-
if (num <= 127) return new Uint8Array([num]);
|
|
1935
|
-
if (num < EIGHT_BITS) return new Uint8Array([204, num]);
|
|
1936
|
-
if (num < SIXTEEN_BITS) {
|
|
1937
|
-
const dv = new DataView(new ArrayBuffer(3));
|
|
1938
|
-
dv.setUint16(1, num);
|
|
1939
|
-
dv.setUint8(0, 205);
|
|
1940
|
-
return new Uint8Array(dv.buffer);
|
|
1941
|
-
}
|
|
1942
|
-
if (num < THIRTY_TWO_BITS) {
|
|
1943
|
-
const dv = new DataView(new ArrayBuffer(5));
|
|
1944
|
-
dv.setUint32(1, num);
|
|
1945
|
-
dv.setUint8(0, 206);
|
|
1946
|
-
return new Uint8Array(dv.buffer);
|
|
1947
|
-
}
|
|
1948
|
-
return encodeFloat64(num);
|
|
1949
|
-
}
|
|
1950
|
-
function encodeSlice(object, byteParts) {
|
|
1951
|
-
if (object === null) {
|
|
1952
|
-
byteParts.push(new Uint8Array([192]));
|
|
1953
|
-
return;
|
|
1954
|
-
}
|
|
1955
|
-
if (object === false) {
|
|
1956
|
-
byteParts.push(new Uint8Array([194]));
|
|
1957
|
-
return;
|
|
1958
|
-
}
|
|
1959
|
-
if (object === true) {
|
|
1960
|
-
byteParts.push(new Uint8Array([195]));
|
|
1961
|
-
return;
|
|
1962
|
-
}
|
|
1963
|
-
if (typeof object === "number") {
|
|
1964
|
-
byteParts.push(encodeNumber(object));
|
|
1965
|
-
return;
|
|
1966
|
-
}
|
|
1967
|
-
if (typeof object === "bigint") {
|
|
1968
|
-
if (object < 0) {
|
|
1969
|
-
if (object < -SIXTY_THREE_BITS)
|
|
1970
|
-
throw new Error("Cannot safely encode bigint larger than 64 bits");
|
|
1971
|
-
const dv2 = new DataView(new ArrayBuffer(9));
|
|
1972
|
-
dv2.setBigInt64(1, object);
|
|
1973
|
-
dv2.setUint8(0, 211);
|
|
1974
|
-
byteParts.push(new Uint8Array(dv2.buffer));
|
|
1975
|
-
return;
|
|
1976
|
-
}
|
|
1977
|
-
if (object >= SIXTY_FOUR_BITS)
|
|
1978
|
-
throw new Error("Cannot safely encode bigint larger than 64 bits");
|
|
1979
|
-
const dv = new DataView(new ArrayBuffer(9));
|
|
1980
|
-
dv.setBigUint64(1, object);
|
|
1981
|
-
dv.setUint8(0, 207);
|
|
1982
|
-
byteParts.push(new Uint8Array(dv.buffer));
|
|
1983
|
-
return;
|
|
1984
|
-
}
|
|
1985
|
-
if (typeof object === "string") {
|
|
1986
|
-
const encoded = textEncoder.encode(object);
|
|
1987
|
-
const len = encoded.length;
|
|
1988
|
-
if (len < FIVE_BITS) byteParts.push(new Uint8Array([160 | len]));
|
|
1989
|
-
else if (len < EIGHT_BITS) byteParts.push(new Uint8Array([217, len]));
|
|
1990
|
-
else if (len < SIXTEEN_BITS) {
|
|
1991
|
-
const dv = new DataView(new ArrayBuffer(3));
|
|
1992
|
-
dv.setUint16(1, len);
|
|
1993
|
-
dv.setUint8(0, 218);
|
|
1994
|
-
byteParts.push(new Uint8Array(dv.buffer));
|
|
1995
|
-
} else if (len < THIRTY_TWO_BITS) {
|
|
1996
|
-
const dv = new DataView(new ArrayBuffer(5));
|
|
1997
|
-
dv.setUint32(1, len);
|
|
1998
|
-
dv.setUint8(0, 219);
|
|
1999
|
-
byteParts.push(new Uint8Array(dv.buffer));
|
|
2000
|
-
} else {
|
|
2001
|
-
throw new Error("Cannot safely encode string with size larger than 32 bits");
|
|
2002
|
-
}
|
|
2003
|
-
byteParts.push(encoded);
|
|
2004
|
-
return;
|
|
2005
|
-
}
|
|
2006
|
-
if (object instanceof Uint8Array) {
|
|
2007
|
-
if (object.length < EIGHT_BITS) {
|
|
2008
|
-
byteParts.push(new Uint8Array([196, object.length]));
|
|
2009
|
-
} else if (object.length < SIXTEEN_BITS) {
|
|
2010
|
-
const dv = new DataView(new ArrayBuffer(3));
|
|
2011
|
-
dv.setUint16(1, object.length);
|
|
2012
|
-
dv.setUint8(0, 197);
|
|
2013
|
-
byteParts.push(new Uint8Array(dv.buffer));
|
|
2014
|
-
} else if (object.length < THIRTY_TWO_BITS) {
|
|
2015
|
-
const dv = new DataView(new ArrayBuffer(5));
|
|
2016
|
-
dv.setUint32(1, object.length);
|
|
2017
|
-
dv.setUint8(0, 198);
|
|
2018
|
-
byteParts.push(new Uint8Array(dv.buffer));
|
|
2019
|
-
} else {
|
|
2020
|
-
throw new Error("Cannot safely encode Uint8Array with size larger than 32 bits");
|
|
2021
|
-
}
|
|
2022
|
-
byteParts.push(object);
|
|
2023
|
-
return;
|
|
2024
|
-
}
|
|
2025
|
-
if (Array.isArray(object)) {
|
|
2026
|
-
if (object.length < FOUR_BITS) byteParts.push(new Uint8Array([144 | object.length]));
|
|
2027
|
-
else if (object.length < SIXTEEN_BITS) {
|
|
2028
|
-
const dv = new DataView(new ArrayBuffer(3));
|
|
2029
|
-
dv.setUint16(1, object.length);
|
|
2030
|
-
dv.setUint8(0, 220);
|
|
2031
|
-
byteParts.push(new Uint8Array(dv.buffer));
|
|
2032
|
-
} else if (object.length < THIRTY_TWO_BITS) {
|
|
2033
|
-
const dv = new DataView(new ArrayBuffer(5));
|
|
2034
|
-
dv.setUint32(1, object.length);
|
|
2035
|
-
dv.setUint8(0, 221);
|
|
2036
|
-
byteParts.push(new Uint8Array(dv.buffer));
|
|
2037
|
-
} else {
|
|
2038
|
-
throw new Error("Cannot safely encode array with size larger than 32 bits");
|
|
2039
|
-
}
|
|
2040
|
-
for (const obj of object) encodeSlice(obj, byteParts);
|
|
2041
|
-
return;
|
|
2042
|
-
}
|
|
2043
|
-
if (typeof object === "object") {
|
|
2044
|
-
const entries = Object.entries(object);
|
|
2045
|
-
const numKeys = entries.length;
|
|
2046
|
-
if (numKeys < FOUR_BITS) byteParts.push(new Uint8Array([128 | numKeys]));
|
|
2047
|
-
else if (numKeys < SIXTEEN_BITS) {
|
|
2048
|
-
const dv = new DataView(new ArrayBuffer(3));
|
|
2049
|
-
dv.setUint16(1, numKeys);
|
|
2050
|
-
dv.setUint8(0, 222);
|
|
2051
|
-
byteParts.push(new Uint8Array(dv.buffer));
|
|
2052
|
-
} else if (numKeys < THIRTY_TWO_BITS) {
|
|
2053
|
-
const dv = new DataView(new ArrayBuffer(5));
|
|
2054
|
-
dv.setUint32(1, numKeys);
|
|
2055
|
-
dv.setUint8(0, 223);
|
|
2056
|
-
byteParts.push(new Uint8Array(dv.buffer));
|
|
2057
|
-
} else {
|
|
2058
|
-
throw new Error("Cannot safely encode map with size larger than 32 bits");
|
|
2059
|
-
}
|
|
2060
|
-
for (const [key, value] of entries) {
|
|
2061
|
-
encodeSlice(key, byteParts);
|
|
2062
|
-
encodeSlice(value, byteParts);
|
|
2063
|
-
}
|
|
2064
|
-
return;
|
|
2065
|
-
}
|
|
2066
|
-
throw new Error("Cannot safely encode value into messagepack");
|
|
2067
|
-
}
|
|
2068
|
-
function largeIntToBigInt(obj) {
|
|
2069
|
-
if (typeof obj === "number" && Number.isInteger(obj) && (obj >= 4294967296 || obj < -2147483648)) {
|
|
2070
|
-
return BigInt(obj);
|
|
2071
|
-
}
|
|
2072
|
-
if (Array.isArray(obj)) return obj.map(largeIntToBigInt);
|
|
2073
|
-
if (typeof obj === "object" && obj !== null && !(obj instanceof Uint8Array)) {
|
|
2074
|
-
const result = {};
|
|
2075
|
-
for (const key in obj) {
|
|
2076
|
-
result[key] = largeIntToBigInt(obj[key]);
|
|
2077
|
-
}
|
|
2078
|
-
return result;
|
|
2079
|
-
}
|
|
2080
|
-
return obj;
|
|
2081
|
-
}
|
|
2082
|
-
function removeUndefinedKeys(obj) {
|
|
2083
|
-
if (Array.isArray(obj)) return obj.map(removeUndefinedKeys);
|
|
2084
|
-
if (typeof obj === "object" && obj !== null && !(obj instanceof Uint8Array)) {
|
|
2085
|
-
const result = {};
|
|
2086
|
-
for (const key in obj) {
|
|
2087
|
-
const val = obj[key];
|
|
2088
|
-
if (val !== void 0) {
|
|
2089
|
-
result[key] = removeUndefinedKeys(val);
|
|
2090
|
-
}
|
|
2091
|
-
}
|
|
2092
|
-
return result;
|
|
2093
|
-
}
|
|
2094
|
-
return obj;
|
|
2095
|
-
}
|
|
2096
|
-
function toUint64Bytes(n) {
|
|
2097
|
-
const bytes = new Uint8Array(8);
|
|
2098
|
-
new DataView(bytes.buffer).setBigUint64(0, BigInt(n));
|
|
2099
|
-
return bytes;
|
|
2100
|
-
}
|
|
2101
|
-
|
|
2102
|
-
// src/perp/signer.ts
|
|
2103
|
-
var HYPERLIQUID_DOMAIN = {
|
|
2104
|
-
name: "Exchange",
|
|
2105
|
-
version: "1",
|
|
2106
|
-
chainId: 1337,
|
|
2107
|
-
verifyingContract: "0x0000000000000000000000000000000000000000"
|
|
2108
|
-
};
|
|
2109
|
-
var HYPERLIQUID_USER_DOMAIN = {
|
|
2110
|
-
name: "HyperliquidSignTransaction",
|
|
2111
|
-
version: "1",
|
|
2112
|
-
chainId: 42161,
|
|
2113
|
-
verifyingContract: "0x0000000000000000000000000000000000000000"
|
|
2114
|
-
};
|
|
2115
|
-
var USER_ACTION_TYPES = {
|
|
2116
|
-
withdraw: {
|
|
2117
|
-
"HyperliquidTransaction:Withdraw": [
|
|
2118
|
-
{ name: "hyperliquidChain", type: "string" },
|
|
2119
|
-
{ name: "destination", type: "string" },
|
|
2120
|
-
{ name: "amount", type: "string" },
|
|
2121
|
-
{ name: "time", type: "uint64" }
|
|
2122
|
-
]
|
|
2123
|
-
},
|
|
2124
|
-
usdSend: {
|
|
2125
|
-
"HyperliquidTransaction:UsdSend": [
|
|
2126
|
-
{ name: "hyperliquidChain", type: "string" },
|
|
2127
|
-
{ name: "destination", type: "string" },
|
|
2128
|
-
{ name: "amount", type: "string" },
|
|
2129
|
-
{ name: "time", type: "uint64" }
|
|
2130
|
-
]
|
|
2131
|
-
}
|
|
2132
|
-
};
|
|
2133
|
-
var PHANTOM_AGENT_TYPES = {
|
|
2134
|
-
Agent: [
|
|
2135
|
-
{ name: "source", type: "string" },
|
|
2136
|
-
{ name: "connectionId", type: "bytes32" }
|
|
2137
|
-
]
|
|
2138
|
-
};
|
|
2139
|
-
var lastNonce = 0n;
|
|
2140
|
-
function getNextNonce() {
|
|
2141
|
-
const now = BigInt(Date.now());
|
|
2142
|
-
if (now <= lastNonce) {
|
|
2143
|
-
lastNonce = lastNonce + 1n;
|
|
2144
|
-
} else {
|
|
2145
|
-
lastNonce = now;
|
|
2146
|
-
}
|
|
2147
|
-
return lastNonce;
|
|
2148
|
-
}
|
|
2149
|
-
function createL1ActionHash(args) {
|
|
2150
|
-
const { action, nonce, vaultAddress } = args;
|
|
2151
|
-
const cleaned = removeUndefinedKeys(action);
|
|
2152
|
-
const withBigInts = largeIntToBigInt(cleaned);
|
|
2153
|
-
const actionBytes = encodeMsgpack(withBigInts);
|
|
2154
|
-
const nonceBytes = toUint64Bytes(nonce);
|
|
2155
|
-
const vaultMarker = vaultAddress ? new Uint8Array([1]) : new Uint8Array([0]);
|
|
2156
|
-
const vaultBytes = vaultAddress ? hexToBytes(vaultAddress.slice(2)) : new Uint8Array();
|
|
2157
|
-
const bytes = concatBytes2(actionBytes, nonceBytes, vaultMarker, vaultBytes);
|
|
2158
|
-
return keccak256(bytes);
|
|
2159
|
-
}
|
|
2160
|
-
var HyperliquidSigner = class {
|
|
2161
|
-
constructor(walletClient, isTestnet = false) {
|
|
2162
|
-
this.walletClient = walletClient;
|
|
2163
|
-
this.isTestnet = isTestnet;
|
|
2164
|
-
}
|
|
2165
|
-
isTestnet;
|
|
2166
|
-
/**
|
|
2167
|
-
* Sign an L1 action (order, cancel, updateLeverage, etc.)
|
|
2168
|
-
* using the phantom agent pattern.
|
|
2169
|
-
*/
|
|
2170
|
-
async signAction(action, nonce, vaultAddress) {
|
|
2171
|
-
const actionNonce = nonce ?? getNextNonce();
|
|
2172
|
-
const nonceNumber = Number(actionNonce);
|
|
2173
|
-
const account = this.walletClient.account;
|
|
2174
|
-
if (!account) throw new Error("Wallet client has no account");
|
|
2175
|
-
const connectionId = createL1ActionHash({
|
|
2176
|
-
action,
|
|
2177
|
-
nonce: nonceNumber,
|
|
2178
|
-
vaultAddress
|
|
2179
|
-
});
|
|
2180
|
-
const signature = await this.walletClient.signTypedData({
|
|
2181
|
-
account,
|
|
2182
|
-
domain: HYPERLIQUID_DOMAIN,
|
|
2183
|
-
types: PHANTOM_AGENT_TYPES,
|
|
2184
|
-
primaryType: "Agent",
|
|
2185
|
-
message: {
|
|
2186
|
-
source: this.isTestnet ? "b" : "a",
|
|
2187
|
-
connectionId
|
|
2188
|
-
}
|
|
2189
|
-
});
|
|
2190
|
-
return { signature, nonce: actionNonce };
|
|
2191
|
-
}
|
|
2192
|
-
/**
|
|
2193
|
-
* Sign a user-signed action (withdraw3, usdSend, approveAgent, etc.)
|
|
2194
|
-
* using the HyperliquidSignTransaction domain (chainId 42161).
|
|
2195
|
-
*
|
|
2196
|
-
* Unlike trading actions, these use direct EIP-712 typed data signing
|
|
2197
|
-
* with action-specific types — NOT the phantom agent pattern.
|
|
2198
|
-
*/
|
|
2199
|
-
async signUserAction(actionType, message) {
|
|
2200
|
-
const account = this.walletClient.account;
|
|
2201
|
-
if (!account) throw new Error("Wallet client has no account");
|
|
2202
|
-
const types = USER_ACTION_TYPES[actionType];
|
|
2203
|
-
const primaryType = Object.keys(types)[0];
|
|
2204
|
-
const signature = await this.walletClient.signTypedData({
|
|
2205
|
-
account,
|
|
2206
|
-
domain: HYPERLIQUID_USER_DOMAIN,
|
|
2207
|
-
types,
|
|
2208
|
-
primaryType,
|
|
2209
|
-
message
|
|
2210
|
-
});
|
|
2211
|
-
return { signature };
|
|
2212
|
-
}
|
|
2213
|
-
/**
|
|
2214
|
-
* Build and sign a withdraw3 action for withdrawing USDC from Hyperliquid to Arbitrum.
|
|
2215
|
-
* Returns the complete payload ready to POST to /exchange.
|
|
2216
|
-
*
|
|
2217
|
-
* @param destination — The Arbitrum address to receive USDC
|
|
2218
|
-
* @param amount — Amount as string (e.g., "10.0" for $10)
|
|
2219
|
-
*/
|
|
2220
|
-
async signWithdraw(destination, amount) {
|
|
2221
|
-
const timestamp = Date.now();
|
|
2222
|
-
const message = {
|
|
2223
|
-
hyperliquidChain: this.isTestnet ? "Testnet" : "Mainnet",
|
|
2224
|
-
destination,
|
|
2225
|
-
amount,
|
|
2226
|
-
time: BigInt(timestamp)
|
|
2227
|
-
};
|
|
2228
|
-
const { signature } = await this.signUserAction("withdraw", message);
|
|
2229
|
-
const action = {
|
|
2230
|
-
type: "withdraw3",
|
|
2231
|
-
hyperliquidChain: this.isTestnet ? "Testnet" : "Mainnet",
|
|
2232
|
-
signatureChainId: "0xa4b1",
|
|
2233
|
-
// Arbitrum chainId in hex
|
|
2234
|
-
destination,
|
|
2235
|
-
amount,
|
|
2236
|
-
time: timestamp
|
|
2237
|
-
};
|
|
2238
|
-
return {
|
|
2239
|
-
action,
|
|
2240
|
-
signature: {
|
|
2241
|
-
r: signature.slice(0, 66),
|
|
2242
|
-
s: `0x${signature.slice(66, 130)}`,
|
|
2243
|
-
v: parseInt(signature.slice(130, 132), 16)
|
|
2244
|
-
},
|
|
2245
|
-
nonce: timestamp
|
|
2246
|
-
};
|
|
2247
|
-
}
|
|
2248
|
-
getAddress() {
|
|
2249
|
-
const account = this.walletClient.account;
|
|
2250
|
-
if (!account) throw new Error("Wallet client has no account");
|
|
2251
|
-
return account.address;
|
|
2252
|
-
}
|
|
2253
|
-
};
|
|
2254
|
-
function hexToBytes(hex) {
|
|
2255
|
-
const bytes = new Uint8Array(hex.length / 2);
|
|
2256
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
2257
|
-
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
2258
|
-
}
|
|
2259
|
-
return bytes;
|
|
2260
|
-
}
|
|
2261
|
-
function concatBytes2(...arrays) {
|
|
2262
|
-
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
|
|
2263
|
-
const result = new Uint8Array(totalLength);
|
|
2264
|
-
let offset = 0;
|
|
2265
|
-
for (const arr of arrays) {
|
|
2266
|
-
result.set(arr, offset);
|
|
2267
|
-
offset += arr.length;
|
|
2268
|
-
}
|
|
2269
|
-
return result;
|
|
2270
|
-
}
|
|
2271
|
-
|
|
2272
|
-
// src/perp/orders.ts
|
|
2273
|
-
var HyperliquidOrderManager = class {
|
|
2274
|
-
client;
|
|
2275
|
-
signer;
|
|
2276
|
-
config;
|
|
2277
|
-
constructor(client, signer, config) {
|
|
2278
|
-
this.client = client;
|
|
2279
|
-
this.signer = signer;
|
|
2280
|
-
this.config = config;
|
|
2281
|
-
}
|
|
2282
|
-
async placeOrder(signal) {
|
|
2283
|
-
try {
|
|
2284
|
-
const assetIndex = await this.client.getAssetIndex(signal.instrument);
|
|
2285
|
-
const isBuy = signal.action === "open_long" || signal.action === "close_short";
|
|
2286
|
-
const orderWire = {
|
|
2287
|
-
a: assetIndex,
|
|
2288
|
-
b: isBuy,
|
|
2289
|
-
p: signal.orderType === "market" ? this.getMarketPrice(signal) : signal.price.toString(),
|
|
2290
|
-
s: signal.size.toString(),
|
|
2291
|
-
r: signal.reduceOnly,
|
|
2292
|
-
t: signal.orderType === "market" ? { limit: { tif: "Ioc" } } : { limit: { tif: "Gtc" } }
|
|
2293
|
-
};
|
|
2294
|
-
const action = {
|
|
2295
|
-
type: "order",
|
|
2296
|
-
orders: [orderWire],
|
|
2297
|
-
grouping: "na"
|
|
2298
|
-
};
|
|
2299
|
-
const nonce = getNextNonce();
|
|
2300
|
-
const { signature } = await this.signer.signAction(action, nonce);
|
|
2301
|
-
const resp = await this.exchangeRequest({
|
|
2302
|
-
action,
|
|
2303
|
-
nonce: Number(nonce),
|
|
2304
|
-
signature: {
|
|
2305
|
-
r: signature.slice(0, 66),
|
|
2306
|
-
s: `0x${signature.slice(66, 130)}`,
|
|
2307
|
-
v: parseInt(signature.slice(130, 132), 16)
|
|
2308
|
-
},
|
|
2309
|
-
vaultAddress: null
|
|
2310
|
-
});
|
|
2311
|
-
return this.parseOrderResponse(resp);
|
|
2312
|
-
} catch (error) {
|
|
2313
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2314
|
-
console.error(`[perp] Order failed for ${signal.instrument}:`, message);
|
|
2315
|
-
return { success: false, status: "error", error: message };
|
|
2316
|
-
}
|
|
2317
|
-
}
|
|
2318
|
-
async cancelOrder(instrument, orderId) {
|
|
2319
|
-
try {
|
|
2320
|
-
const assetIndex = await this.client.getAssetIndex(instrument);
|
|
2321
|
-
const action = {
|
|
2322
|
-
type: "cancel",
|
|
2323
|
-
cancels: [{ a: assetIndex, o: orderId }]
|
|
2324
|
-
};
|
|
2325
|
-
const nonce = getNextNonce();
|
|
2326
|
-
const { signature } = await this.signer.signAction(action, nonce);
|
|
2327
|
-
await this.exchangeRequest({
|
|
2328
|
-
action,
|
|
2329
|
-
nonce: Number(nonce),
|
|
2330
|
-
signature: {
|
|
2331
|
-
r: signature.slice(0, 66),
|
|
2332
|
-
s: `0x${signature.slice(66, 130)}`,
|
|
2333
|
-
v: parseInt(signature.slice(130, 132), 16)
|
|
2334
|
-
},
|
|
2335
|
-
vaultAddress: null
|
|
2336
|
-
});
|
|
2337
|
-
console.log(`[perp] Cancelled order ${orderId} for ${instrument}`);
|
|
2338
|
-
return true;
|
|
2339
|
-
} catch (error) {
|
|
2340
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2341
|
-
console.error(`[perp] Cancel failed for order ${orderId}:`, message);
|
|
2342
|
-
return false;
|
|
2343
|
-
}
|
|
2344
|
-
}
|
|
2345
|
-
async closePosition(instrument, positionSize) {
|
|
2346
|
-
const isLong = positionSize > 0;
|
|
2347
|
-
return this.placeOrder({
|
|
2348
|
-
action: isLong ? "close_long" : "close_short",
|
|
2349
|
-
instrument,
|
|
2350
|
-
size: Math.abs(positionSize),
|
|
2351
|
-
price: 0,
|
|
2352
|
-
leverage: 1,
|
|
2353
|
-
orderType: "market",
|
|
2354
|
-
reduceOnly: true,
|
|
2355
|
-
confidence: 1,
|
|
2356
|
-
reasoning: "Position close"
|
|
2357
|
-
});
|
|
2358
|
-
}
|
|
2359
|
-
async updateLeverage(instrument, leverage, isCross = true) {
|
|
2360
|
-
try {
|
|
2361
|
-
const assetIndex = await this.client.getAssetIndex(instrument);
|
|
2362
|
-
const action = {
|
|
2363
|
-
type: "updateLeverage",
|
|
2364
|
-
asset: assetIndex,
|
|
2365
|
-
isCross,
|
|
2366
|
-
leverage
|
|
2367
|
-
};
|
|
2368
|
-
const nonce = getNextNonce();
|
|
2369
|
-
const { signature } = await this.signer.signAction(action, nonce);
|
|
2370
|
-
await this.exchangeRequest({
|
|
2371
|
-
action,
|
|
2372
|
-
nonce: Number(nonce),
|
|
2373
|
-
signature: {
|
|
2374
|
-
r: signature.slice(0, 66),
|
|
2375
|
-
s: `0x${signature.slice(66, 130)}`,
|
|
2376
|
-
v: parseInt(signature.slice(130, 132), 16)
|
|
2377
|
-
},
|
|
2378
|
-
vaultAddress: null
|
|
2379
|
-
});
|
|
2380
|
-
console.log(`[perp] Leverage set for ${instrument}: ${leverage}x (${isCross ? "cross" : "isolated"})`);
|
|
2381
|
-
return true;
|
|
2382
|
-
} catch (error) {
|
|
2383
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2384
|
-
console.error(`[perp] Leverage update failed for ${instrument}:`, message);
|
|
2385
|
-
return false;
|
|
2386
|
-
}
|
|
2387
|
-
}
|
|
2388
|
-
// ── PRIVATE ────────────────────────────────────────────────
|
|
2389
|
-
getMarketPrice(signal) {
|
|
2390
|
-
const isBuy = signal.action === "open_long" || signal.action === "close_short";
|
|
2391
|
-
if (signal.price > 0) {
|
|
2392
|
-
const slippage = isBuy ? 1.005 : 0.995;
|
|
2393
|
-
return (signal.price * slippage).toString();
|
|
2394
|
-
}
|
|
2395
|
-
return "0";
|
|
2396
|
-
}
|
|
2397
|
-
parseOrderResponse(resp) {
|
|
2398
|
-
if (resp?.status === "ok" && resp?.response?.type === "order") {
|
|
2399
|
-
const statuses = resp.response.data?.statuses || [];
|
|
2400
|
-
if (statuses.length > 0) {
|
|
2401
|
-
const status = statuses[0];
|
|
2402
|
-
if (status.filled) {
|
|
2403
|
-
return {
|
|
2404
|
-
success: true,
|
|
2405
|
-
orderId: status.filled.oid,
|
|
2406
|
-
status: "filled",
|
|
2407
|
-
avgPrice: status.filled.avgPx,
|
|
2408
|
-
filledSize: status.filled.totalSz
|
|
2409
|
-
};
|
|
2410
|
-
}
|
|
2411
|
-
if (status.resting) {
|
|
2412
|
-
return {
|
|
2413
|
-
success: true,
|
|
2414
|
-
orderId: status.resting.oid,
|
|
2415
|
-
status: "resting"
|
|
2416
|
-
};
|
|
2417
|
-
}
|
|
2418
|
-
if (status.error) {
|
|
2419
|
-
return { success: false, status: "error", error: status.error };
|
|
2420
|
-
}
|
|
2421
|
-
}
|
|
2422
|
-
}
|
|
2423
|
-
return {
|
|
2424
|
-
success: false,
|
|
2425
|
-
status: "error",
|
|
2426
|
-
error: `Unexpected response: ${JSON.stringify(resp)}`
|
|
2427
|
-
};
|
|
2428
|
-
}
|
|
2429
|
-
async exchangeRequest(body) {
|
|
2430
|
-
const resp = await fetch(`${this.config.apiUrl}/exchange`, {
|
|
2431
|
-
method: "POST",
|
|
2432
|
-
headers: { "Content-Type": "application/json" },
|
|
2433
|
-
body: JSON.stringify(body)
|
|
2434
|
-
});
|
|
2435
|
-
if (!resp.ok) {
|
|
2436
|
-
throw new Error(`Hyperliquid Exchange API error: ${resp.status} ${await resp.text()}`);
|
|
2437
|
-
}
|
|
2438
|
-
return resp.json();
|
|
2439
|
-
}
|
|
2440
|
-
};
|
|
2441
|
-
|
|
2442
|
-
// src/perp/positions.ts
|
|
2443
|
-
var HyperliquidPositionManager = class {
|
|
2444
|
-
client;
|
|
2445
|
-
userAddress;
|
|
2446
|
-
cachedPositions = [];
|
|
2447
|
-
cachedAccount = null;
|
|
2448
|
-
lastRefreshAt = 0;
|
|
2449
|
-
cacheTtlMs = 5e3;
|
|
2450
|
-
constructor(client, userAddress) {
|
|
2451
|
-
this.client = client;
|
|
2452
|
-
this.userAddress = userAddress;
|
|
2453
|
-
}
|
|
2454
|
-
// ── POSITION QUERIES ───────────────────────────────────────
|
|
2455
|
-
async getPositions(forceRefresh = false) {
|
|
2456
|
-
if (!forceRefresh && this.isCacheFresh()) return this.cachedPositions;
|
|
2457
|
-
await this.refresh();
|
|
2458
|
-
return this.cachedPositions;
|
|
2459
|
-
}
|
|
2460
|
-
async getPosition(instrument) {
|
|
2461
|
-
const positions = await this.getPositions();
|
|
2462
|
-
return positions.find((p) => p.instrument === instrument) ?? null;
|
|
2463
|
-
}
|
|
2464
|
-
async getAccountSummary(forceRefresh = false) {
|
|
2465
|
-
if (!forceRefresh && this.isCacheFresh() && this.cachedAccount) {
|
|
2466
|
-
return this.cachedAccount;
|
|
2467
|
-
}
|
|
2468
|
-
await this.refresh();
|
|
2469
|
-
return this.cachedAccount;
|
|
2470
|
-
}
|
|
2471
|
-
// ── LIQUIDATION MONITORING ─────────────────────────────────
|
|
2472
|
-
async getLiquidationProximity() {
|
|
2473
|
-
const positions = await this.getPositions();
|
|
2474
|
-
const proximities = /* @__PURE__ */ new Map();
|
|
2475
|
-
for (const pos of positions) {
|
|
2476
|
-
if (pos.liquidationPrice <= 0 || pos.markPrice <= 0) {
|
|
2477
|
-
proximities.set(pos.instrument, 0);
|
|
2478
|
-
continue;
|
|
2479
|
-
}
|
|
2480
|
-
let proximity;
|
|
2481
|
-
if (pos.size > 0) {
|
|
2482
|
-
if (pos.markPrice <= pos.liquidationPrice) {
|
|
2483
|
-
proximity = 1;
|
|
2484
|
-
} else {
|
|
2485
|
-
const distanceToLiq = pos.markPrice - pos.liquidationPrice;
|
|
2486
|
-
const entryToLiq = pos.entryPrice - pos.liquidationPrice;
|
|
2487
|
-
proximity = entryToLiq > 0 ? 1 - distanceToLiq / entryToLiq : 0;
|
|
2488
|
-
}
|
|
2489
|
-
} else {
|
|
2490
|
-
if (pos.markPrice >= pos.liquidationPrice) {
|
|
2491
|
-
proximity = 1;
|
|
2492
|
-
} else {
|
|
2493
|
-
const distanceToLiq = pos.liquidationPrice - pos.markPrice;
|
|
2494
|
-
const entryToLiq = pos.liquidationPrice - pos.entryPrice;
|
|
2495
|
-
proximity = entryToLiq > 0 ? 1 - distanceToLiq / entryToLiq : 0;
|
|
2496
|
-
}
|
|
2497
|
-
}
|
|
2498
|
-
proximities.set(pos.instrument, Math.max(0, Math.min(1, proximity)));
|
|
2499
|
-
}
|
|
2500
|
-
return proximities;
|
|
2501
|
-
}
|
|
2502
|
-
async getDangerousPositions(threshold = 0.7) {
|
|
2503
|
-
const positions = await this.getPositions();
|
|
2504
|
-
const proximities = await this.getLiquidationProximity();
|
|
2505
|
-
return positions.filter((p) => (proximities.get(p.instrument) ?? 0) > threshold);
|
|
2506
|
-
}
|
|
2507
|
-
// ── SUMMARY HELPERS ────────────────────────────────────────
|
|
2508
|
-
async getTotalUnrealizedPnl() {
|
|
2509
|
-
const positions = await this.getPositions();
|
|
2510
|
-
return positions.reduce((sum, p) => sum + p.unrealizedPnl, 0);
|
|
2511
|
-
}
|
|
2512
|
-
async getTotalNotional() {
|
|
2513
|
-
const positions = await this.getPositions();
|
|
2514
|
-
return positions.reduce((sum, p) => sum + p.notionalUSD, 0);
|
|
2515
|
-
}
|
|
2516
|
-
async getPositionCount() {
|
|
2517
|
-
return (await this.getPositions()).length;
|
|
2518
|
-
}
|
|
2519
|
-
// ── CACHE ──────────────────────────────────────────────────
|
|
2520
|
-
async refresh() {
|
|
2521
|
-
try {
|
|
2522
|
-
const [positions, account] = await Promise.all([
|
|
2523
|
-
this.client.getPositions(this.userAddress),
|
|
2524
|
-
this.client.getAccountSummary(this.userAddress)
|
|
2525
|
-
]);
|
|
2526
|
-
this.cachedPositions = positions;
|
|
2527
|
-
this.cachedAccount = account;
|
|
2528
|
-
this.lastRefreshAt = Date.now();
|
|
2529
|
-
} catch (error) {
|
|
2530
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2531
|
-
console.error("[perp] Failed to refresh positions:", message);
|
|
2532
|
-
}
|
|
2533
|
-
}
|
|
2534
|
-
isCacheFresh() {
|
|
2535
|
-
return Date.now() - this.lastRefreshAt < this.cacheTtlMs;
|
|
2536
|
-
}
|
|
2537
|
-
};
|
|
2538
|
-
|
|
2539
|
-
// src/perp/websocket.ts
|
|
2540
|
-
import WebSocket2 from "ws";
|
|
2541
|
-
var HyperliquidWebSocket = class {
|
|
2542
|
-
wsUrl;
|
|
2543
|
-
userAddress;
|
|
2544
|
-
client;
|
|
2545
|
-
ws = null;
|
|
2546
|
-
reconnectAttempts = 0;
|
|
2547
|
-
maxReconnectAttempts = 20;
|
|
2548
|
-
baseReconnectMs = 1e3;
|
|
2549
|
-
maxReconnectMs = 6e4;
|
|
2550
|
-
reconnectTimer = null;
|
|
2551
|
-
pingTimer = null;
|
|
2552
|
-
isConnecting = false;
|
|
2553
|
-
shouldReconnect = true;
|
|
2554
|
-
lastProcessedFillTime = 0;
|
|
2555
|
-
onFill = null;
|
|
2556
|
-
onFunding = null;
|
|
2557
|
-
onLiquidation = null;
|
|
2558
|
-
constructor(config, userAddress, client) {
|
|
2559
|
-
this.wsUrl = config.wsUrl;
|
|
2560
|
-
this.userAddress = userAddress;
|
|
2561
|
-
this.client = client;
|
|
2562
|
-
}
|
|
2563
|
-
// ── CONNECTION ─────────────────────────────────────────────
|
|
2564
|
-
async connect() {
|
|
2565
|
-
if (this.ws?.readyState === WebSocket2.OPEN || this.isConnecting) return;
|
|
2566
|
-
this.isConnecting = true;
|
|
2567
|
-
this.shouldReconnect = true;
|
|
2568
|
-
return new Promise((resolve2, reject) => {
|
|
2569
|
-
try {
|
|
2570
|
-
this.ws = new WebSocket2(this.wsUrl);
|
|
2571
|
-
this.ws.on("open", () => {
|
|
2572
|
-
this.isConnecting = false;
|
|
2573
|
-
this.reconnectAttempts = 0;
|
|
2574
|
-
console.log("[perp-ws] Connected");
|
|
2575
|
-
this.subscribe({
|
|
2576
|
-
type: "subscribe",
|
|
2577
|
-
subscription: { type: "userFills", user: this.userAddress }
|
|
2578
|
-
});
|
|
2579
|
-
this.subscribe({
|
|
2580
|
-
type: "subscribe",
|
|
2581
|
-
subscription: { type: "userFundings", user: this.userAddress }
|
|
2582
|
-
});
|
|
2583
|
-
this.startPing();
|
|
2584
|
-
this.backfillMissedFills().catch((err) => {
|
|
2585
|
-
console.warn("[perp-ws] Backfill failed:", err instanceof Error ? err.message : err);
|
|
2586
|
-
});
|
|
2587
|
-
resolve2();
|
|
2588
|
-
});
|
|
2589
|
-
this.ws.on("message", (data) => {
|
|
2590
|
-
this.handleMessage(data);
|
|
2591
|
-
});
|
|
2592
|
-
this.ws.on("close", (code, reason) => {
|
|
2593
|
-
this.isConnecting = false;
|
|
2594
|
-
console.log(`[perp-ws] Closed: ${code} ${reason.toString()}`);
|
|
2595
|
-
this.stopPing();
|
|
2596
|
-
this.scheduleReconnect();
|
|
2597
|
-
});
|
|
2598
|
-
this.ws.on("error", (error) => {
|
|
2599
|
-
this.isConnecting = false;
|
|
2600
|
-
console.error("[perp-ws] Error:", error.message);
|
|
2601
|
-
if (this.reconnectAttempts === 0) reject(error);
|
|
2602
|
-
});
|
|
2603
|
-
} catch (error) {
|
|
2604
|
-
this.isConnecting = false;
|
|
2605
|
-
reject(error);
|
|
2606
|
-
}
|
|
2607
|
-
});
|
|
2608
|
-
}
|
|
2609
|
-
disconnect() {
|
|
2610
|
-
this.shouldReconnect = false;
|
|
2611
|
-
if (this.reconnectTimer) {
|
|
2612
|
-
clearTimeout(this.reconnectTimer);
|
|
2613
|
-
this.reconnectTimer = null;
|
|
2614
|
-
}
|
|
2615
|
-
this.stopPing();
|
|
2616
|
-
if (this.ws) {
|
|
2617
|
-
this.ws.removeAllListeners();
|
|
2618
|
-
if (this.ws.readyState === WebSocket2.OPEN) {
|
|
2619
|
-
this.ws.close(1e3, "Client disconnect");
|
|
2620
|
-
}
|
|
2621
|
-
this.ws = null;
|
|
2622
|
-
}
|
|
2623
|
-
console.log("[perp-ws] Disconnected");
|
|
2624
|
-
}
|
|
2625
|
-
get isConnected() {
|
|
2626
|
-
return this.ws?.readyState === WebSocket2.OPEN;
|
|
2627
|
-
}
|
|
2628
|
-
// ── EVENT HANDLERS ─────────────────────────────────────────
|
|
2629
|
-
onFillReceived(callback) {
|
|
2630
|
-
this.onFill = callback;
|
|
2631
|
-
}
|
|
2632
|
-
onFundingReceived(callback) {
|
|
2633
|
-
this.onFunding = callback;
|
|
2634
|
-
}
|
|
2635
|
-
onLiquidationDetected(callback) {
|
|
2636
|
-
this.onLiquidation = callback;
|
|
2637
|
-
}
|
|
2638
|
-
getLastProcessedFillTime() {
|
|
2639
|
-
return this.lastProcessedFillTime;
|
|
2640
|
-
}
|
|
2641
|
-
// ── MESSAGE HANDLING ───────────────────────────────────────
|
|
2642
|
-
handleMessage(data) {
|
|
2643
|
-
try {
|
|
2644
|
-
const msg = JSON.parse(data.toString());
|
|
2645
|
-
if (msg.channel === "userFills") {
|
|
2646
|
-
this.handleFillMessage(msg.data);
|
|
2647
|
-
} else if (msg.channel === "userFundings") {
|
|
2648
|
-
this.handleFundingMessage(msg.data);
|
|
2649
|
-
}
|
|
2650
|
-
} catch {
|
|
2651
|
-
}
|
|
2652
|
-
}
|
|
2653
|
-
handleFillMessage(fills) {
|
|
2654
|
-
if (!Array.isArray(fills) || !this.onFill) return;
|
|
2655
|
-
for (const rawFill of fills) {
|
|
2656
|
-
const fill = this.client.parseFill(rawFill);
|
|
2657
|
-
if (fill.time > this.lastProcessedFillTime) {
|
|
2658
|
-
this.lastProcessedFillTime = fill.time;
|
|
2659
|
-
}
|
|
2660
|
-
if (fill.liquidation && this.onLiquidation) {
|
|
2661
|
-
this.onLiquidation(fill.coin, parseFloat(fill.sz));
|
|
2662
|
-
}
|
|
2663
|
-
this.onFill(fill);
|
|
2664
|
-
}
|
|
2665
|
-
}
|
|
2666
|
-
handleFundingMessage(fundings) {
|
|
2667
|
-
if (!Array.isArray(fundings) || !this.onFunding) return;
|
|
2668
|
-
for (const funding of fundings) {
|
|
2669
|
-
this.onFunding({
|
|
2670
|
-
time: funding.time,
|
|
2671
|
-
coin: funding.coin,
|
|
2672
|
-
usdc: funding.usdc,
|
|
2673
|
-
szi: funding.szi,
|
|
2674
|
-
fundingRate: funding.fundingRate
|
|
2675
|
-
});
|
|
2676
|
-
}
|
|
2677
|
-
}
|
|
2678
|
-
// ── BACKFILL ───────────────────────────────────────────────
|
|
2679
|
-
async backfillMissedFills() {
|
|
2680
|
-
if (this.lastProcessedFillTime === 0 || !this.onFill) return;
|
|
2681
|
-
console.log(`[perp-ws] Backfilling fills since ${new Date(this.lastProcessedFillTime).toISOString()}`);
|
|
2682
|
-
const fills = await this.client.getUserFillsByTime(
|
|
2683
|
-
this.userAddress,
|
|
2684
|
-
this.lastProcessedFillTime + 1
|
|
2685
|
-
);
|
|
2686
|
-
if (fills.length > 0) {
|
|
2687
|
-
console.log(`[perp-ws] Backfilled ${fills.length} missed fills`);
|
|
2688
|
-
for (const rawFill of fills) {
|
|
2689
|
-
const fill = this.client.parseFill(rawFill);
|
|
2690
|
-
if (fill.time > this.lastProcessedFillTime) {
|
|
2691
|
-
this.lastProcessedFillTime = fill.time;
|
|
2692
|
-
}
|
|
2693
|
-
if (fill.liquidation && this.onLiquidation) {
|
|
2694
|
-
this.onLiquidation(fill.coin, parseFloat(fill.sz));
|
|
2695
|
-
}
|
|
2696
|
-
this.onFill(fill);
|
|
2697
|
-
}
|
|
2698
|
-
}
|
|
2699
|
-
}
|
|
2700
|
-
// ── RECONNECTION ───────────────────────────────────────────
|
|
2701
|
-
scheduleReconnect() {
|
|
2702
|
-
if (!this.shouldReconnect || this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
2703
|
-
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
2704
|
-
console.error(`[perp-ws] Max reconnect attempts (${this.maxReconnectAttempts}) reached`);
|
|
2705
|
-
}
|
|
2706
|
-
return;
|
|
2707
|
-
}
|
|
2708
|
-
const delay = Math.min(
|
|
2709
|
-
this.baseReconnectMs * Math.pow(2, this.reconnectAttempts),
|
|
2710
|
-
this.maxReconnectMs
|
|
2711
|
-
);
|
|
2712
|
-
this.reconnectAttempts++;
|
|
2713
|
-
console.log(`[perp-ws] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
2714
|
-
this.reconnectTimer = setTimeout(() => {
|
|
2715
|
-
this.connect().catch((err) => {
|
|
2716
|
-
console.error("[perp-ws] Reconnect failed:", err instanceof Error ? err.message : err);
|
|
2717
|
-
});
|
|
2718
|
-
}, delay);
|
|
2719
|
-
}
|
|
2720
|
-
// ── KEEPALIVE ──────────────────────────────────────────────
|
|
2721
|
-
startPing() {
|
|
2722
|
-
this.stopPing();
|
|
2723
|
-
this.pingTimer = setInterval(() => {
|
|
2724
|
-
if (this.ws?.readyState === WebSocket2.OPEN) {
|
|
2725
|
-
this.ws.send(JSON.stringify({ method: "ping" }));
|
|
2726
|
-
}
|
|
2727
|
-
}, 25e3);
|
|
2728
|
-
}
|
|
2729
|
-
stopPing() {
|
|
2730
|
-
if (this.pingTimer) {
|
|
2731
|
-
clearInterval(this.pingTimer);
|
|
2732
|
-
this.pingTimer = null;
|
|
2733
|
-
}
|
|
2734
|
-
}
|
|
2735
|
-
// ── HELPERS ────────────────────────────────────────────────
|
|
2736
|
-
subscribe(msg) {
|
|
2737
|
-
if (this.ws?.readyState === WebSocket2.OPEN) {
|
|
2738
|
-
this.ws.send(JSON.stringify(msg));
|
|
2739
|
-
}
|
|
2740
|
-
}
|
|
2741
|
-
};
|
|
2742
|
-
|
|
2743
|
-
// src/prediction/types.ts
|
|
2744
|
-
var DEFAULT_PREDICTION_CONFIG = {
|
|
2745
|
-
enabled: false,
|
|
2746
|
-
clobApiUrl: "https://clob.polymarket.com",
|
|
2747
|
-
gammaApiUrl: "https://gamma-api.polymarket.com",
|
|
2748
|
-
maxNotionalUSD: 1e3,
|
|
2749
|
-
maxTotalExposureUSD: 5e3
|
|
2750
|
-
};
|
|
2751
|
-
var POLYGON_CHAIN_ID = 137;
|
|
2752
|
-
var PREDICTION_INSTRUMENT_PREFIX = "POLY:";
|
|
2753
|
-
function encodePredictionInstrument(conditionId, outcomeIndex) {
|
|
2754
|
-
return `${PREDICTION_INSTRUMENT_PREFIX}${conditionId}:${outcomeIndex}`;
|
|
2755
|
-
}
|
|
2756
|
-
function decodePredictionInstrument(instrument) {
|
|
2757
|
-
if (!instrument.startsWith(PREDICTION_INSTRUMENT_PREFIX)) return null;
|
|
2758
|
-
const parts = instrument.slice(PREDICTION_INSTRUMENT_PREFIX.length).split(":");
|
|
2759
|
-
if (parts.length !== 2) return null;
|
|
2760
|
-
return { conditionId: parts[0], outcomeIndex: parseInt(parts[1], 10) };
|
|
2761
|
-
}
|
|
2762
|
-
|
|
2763
|
-
// src/prediction/client.ts
|
|
2764
|
-
import { ClobClient } from "@polymarket/clob-client";
|
|
2765
|
-
import { Wallet } from "ethers";
|
|
2766
|
-
var PolymarketClient = class {
|
|
2767
|
-
config;
|
|
2768
|
-
clobClient = null;
|
|
2769
|
-
apiCreds = null;
|
|
2770
|
-
signer;
|
|
2771
|
-
walletAddress;
|
|
2772
|
-
marketCache = /* @__PURE__ */ new Map();
|
|
2773
|
-
CACHE_TTL_MS = 6e4;
|
|
2774
|
-
constructor(privateKey, config) {
|
|
2775
|
-
this.config = { ...DEFAULT_PREDICTION_CONFIG, ...config };
|
|
2776
|
-
this.signer = new Wallet(privateKey);
|
|
2777
|
-
this.walletAddress = this.signer.address;
|
|
2778
|
-
}
|
|
2779
|
-
// ── INITIALIZATION ─────────────────────────────────────────
|
|
2780
|
-
async initialize() {
|
|
2781
|
-
const initClient = new ClobClient(
|
|
2782
|
-
this.config.clobApiUrl,
|
|
2783
|
-
POLYGON_CHAIN_ID,
|
|
2784
|
-
this.signer
|
|
2785
|
-
);
|
|
2786
|
-
this.apiCreds = await initClient.createOrDeriveApiKey();
|
|
2787
|
-
this.clobClient = new ClobClient(
|
|
2788
|
-
this.config.clobApiUrl,
|
|
2789
|
-
POLYGON_CHAIN_ID,
|
|
2790
|
-
this.signer,
|
|
2791
|
-
this.apiCreds,
|
|
2792
|
-
0
|
|
2793
|
-
);
|
|
2794
|
-
console.log(`[prediction] CLOB initialized for ${this.walletAddress}`);
|
|
2795
|
-
}
|
|
2796
|
-
get isInitialized() {
|
|
2797
|
-
return this.clobClient !== null && this.apiCreds !== null;
|
|
2798
|
-
}
|
|
2799
|
-
// ── CLOB API — ORDER BOOK & PRICES ─────────────────────────
|
|
2800
|
-
async getOrderBook(tokenId) {
|
|
2801
|
-
this.ensureInitialized();
|
|
2802
|
-
const book = await this.clobClient.getOrderBook(tokenId);
|
|
2803
|
-
return {
|
|
2804
|
-
bids: (book.bids || []).map((b) => ({ price: parseFloat(b.price), size: parseFloat(b.size) })),
|
|
2805
|
-
asks: (book.asks || []).map((a) => ({ price: parseFloat(a.price), size: parseFloat(a.size) }))
|
|
2806
|
-
};
|
|
2807
|
-
}
|
|
2808
|
-
async getMidpointPrice(tokenId) {
|
|
2809
|
-
const book = await this.getOrderBook(tokenId);
|
|
2810
|
-
if (book.bids.length === 0 || book.asks.length === 0) return 0;
|
|
2811
|
-
return (book.bids[0].price + book.asks[0].price) / 2;
|
|
2812
|
-
}
|
|
2813
|
-
async getLastTradePrice(tokenId) {
|
|
2814
|
-
this.ensureInitialized();
|
|
2815
|
-
const resp = await this.clobClient.getLastTradePrice(tokenId);
|
|
2816
|
-
return parseFloat(resp?.price || "0");
|
|
2817
|
-
}
|
|
2818
|
-
// ── CLOB API — ORDERS ──────────────────────────────────────
|
|
2819
|
-
async placeLimitOrder(params) {
|
|
2820
|
-
this.ensureInitialized();
|
|
2821
|
-
const order = await this.clobClient.createAndPostOrder({
|
|
2822
|
-
tokenID: params.tokenId,
|
|
2823
|
-
price: params.price,
|
|
2824
|
-
side: params.side,
|
|
2825
|
-
size: params.size
|
|
2826
|
-
});
|
|
2827
|
-
return {
|
|
2828
|
-
orderId: order?.orderID || "",
|
|
2829
|
-
success: !!order?.orderID
|
|
2830
|
-
};
|
|
2831
|
-
}
|
|
2832
|
-
async placeMarketOrder(params) {
|
|
2833
|
-
this.ensureInitialized();
|
|
2834
|
-
const order = await this.clobClient.createMarketOrder({
|
|
2835
|
-
tokenID: params.tokenId,
|
|
2836
|
-
amount: params.amount,
|
|
2837
|
-
side: params.side
|
|
2838
|
-
});
|
|
2839
|
-
const result = await this.clobClient.postOrder(order);
|
|
2840
|
-
return {
|
|
2841
|
-
orderId: result?.orderID || "",
|
|
2842
|
-
success: !!result?.orderID
|
|
2843
|
-
};
|
|
2844
|
-
}
|
|
2845
|
-
async cancelOrder(orderId) {
|
|
2846
|
-
this.ensureInitialized();
|
|
2847
|
-
try {
|
|
2848
|
-
await this.clobClient.cancelOrder({ orderID: orderId });
|
|
2849
|
-
return true;
|
|
2850
|
-
} catch {
|
|
2851
|
-
return false;
|
|
2852
|
-
}
|
|
2853
|
-
}
|
|
2854
|
-
async cancelAllOrders() {
|
|
2855
|
-
this.ensureInitialized();
|
|
2856
|
-
try {
|
|
2857
|
-
await this.clobClient.cancelAll();
|
|
2858
|
-
return true;
|
|
2859
|
-
} catch {
|
|
2860
|
-
return false;
|
|
2861
|
-
}
|
|
2862
|
-
}
|
|
2863
|
-
async getOpenOrders() {
|
|
2864
|
-
this.ensureInitialized();
|
|
2865
|
-
return this.clobClient.getOpenOrders();
|
|
2866
|
-
}
|
|
2867
|
-
async getTradeHistory() {
|
|
2868
|
-
this.ensureInitialized();
|
|
2869
|
-
const trades = await this.clobClient.getTrades();
|
|
2870
|
-
return (trades || []).map((t) => this.parseRawFill(t));
|
|
2871
|
-
}
|
|
2872
|
-
// ── GAMMA API — MARKET DISCOVERY (public) ──────────────────
|
|
2873
|
-
async getMarkets(params) {
|
|
2874
|
-
const query = new URLSearchParams();
|
|
2875
|
-
if (params?.limit) query.set("limit", String(params.limit));
|
|
2876
|
-
if (params?.offset) query.set("offset", String(params.offset));
|
|
2877
|
-
if (params?.active !== void 0) query.set("active", String(params.active));
|
|
2878
|
-
if (params?.category) query.set("tag", params.category);
|
|
2879
|
-
const url = `${this.config.gammaApiUrl}/markets?${query.toString()}`;
|
|
2880
|
-
const resp = await fetch(url);
|
|
2881
|
-
if (!resp.ok) throw new Error(`Gamma API error: ${resp.status} ${await resp.text()}`);
|
|
2882
|
-
const raw = await resp.json();
|
|
2883
|
-
return (raw || []).map((m) => this.parseGammaMarket(m));
|
|
2884
|
-
}
|
|
2885
|
-
async getMarketByConditionId(conditionId) {
|
|
2886
|
-
const cached = this.marketCache.get(conditionId);
|
|
2887
|
-
if (cached && Date.now() - cached.cachedAt < this.CACHE_TTL_MS) {
|
|
2888
|
-
return cached.market;
|
|
2889
|
-
}
|
|
2890
|
-
const url = `${this.config.gammaApiUrl}/markets?condition_id=${conditionId}`;
|
|
2891
|
-
const resp = await fetch(url);
|
|
2892
|
-
if (!resp.ok) return null;
|
|
2893
|
-
const raw = await resp.json();
|
|
2894
|
-
if (!raw || raw.length === 0) return null;
|
|
2895
|
-
const market = this.parseGammaMarket(raw[0]);
|
|
2896
|
-
this.marketCache.set(conditionId, { market, cachedAt: Date.now() });
|
|
2897
|
-
return market;
|
|
2898
|
-
}
|
|
2899
|
-
async searchMarkets(query, limit = 20) {
|
|
2900
|
-
const url = `${this.config.gammaApiUrl}/markets?_q=${encodeURIComponent(query)}&limit=${limit}&active=true`;
|
|
2901
|
-
const resp = await fetch(url);
|
|
2902
|
-
if (!resp.ok) return [];
|
|
2903
|
-
const raw = await resp.json();
|
|
2904
|
-
return (raw || []).map((m) => this.parseGammaMarket(m));
|
|
2905
|
-
}
|
|
2906
|
-
async getTrendingMarkets(limit = 10) {
|
|
2907
|
-
const url = `${this.config.gammaApiUrl}/markets?active=true&limit=${limit}&order=volume24hr&ascending=false`;
|
|
2908
|
-
const resp = await fetch(url);
|
|
2909
|
-
if (!resp.ok) return [];
|
|
2910
|
-
const raw = await resp.json();
|
|
2911
|
-
return (raw || []).map((m) => this.parseGammaMarket(m));
|
|
2912
|
-
}
|
|
2913
|
-
// ── WALLET ─────────────────────────────────────────────────
|
|
2914
|
-
getWalletAddress() {
|
|
2915
|
-
return this.walletAddress;
|
|
2916
|
-
}
|
|
2917
|
-
// ── PRIVATE ────────────────────────────────────────────────
|
|
2918
|
-
ensureInitialized() {
|
|
2919
|
-
if (!this.clobClient) {
|
|
2920
|
-
throw new Error("PolymarketClient not initialized. Call initialize() first.");
|
|
2921
|
-
}
|
|
2922
|
-
}
|
|
2923
|
-
parseGammaMarket(raw) {
|
|
2924
|
-
const outcomes = raw.outcomes ? JSON.parse(raw.outcomes) : ["Yes", "No"];
|
|
2925
|
-
const outcomePrices = raw.outcomePrices ? JSON.parse(raw.outcomePrices) : [0, 0];
|
|
2926
|
-
const outcomeTokenIds = raw.clobTokenIds ? JSON.parse(raw.clobTokenIds) : [];
|
|
2927
|
-
return {
|
|
2928
|
-
conditionId: raw.conditionId || raw.condition_id || "",
|
|
2929
|
-
question: raw.question || "",
|
|
2930
|
-
description: raw.description || "",
|
|
2931
|
-
category: raw.groupItemTitle || raw.category || "Other",
|
|
2932
|
-
outcomes,
|
|
2933
|
-
outcomeTokenIds,
|
|
2934
|
-
outcomePrices: outcomePrices.map((p) => parseFloat(p) || 0),
|
|
2935
|
-
volume24h: parseFloat(raw.volume24hr || raw.volume24h || "0"),
|
|
2936
|
-
liquidity: parseFloat(raw.liquidity || "0"),
|
|
2937
|
-
endDate: raw.endDate ? new Date(raw.endDate).getTime() / 1e3 : 0,
|
|
2938
|
-
active: raw.active !== false && raw.closed !== true,
|
|
2939
|
-
resolved: raw.resolved === true,
|
|
2940
|
-
winningOutcome: raw.winningOutcome,
|
|
2941
|
-
resolutionSource: raw.resolutionSource || void 0,
|
|
2942
|
-
uniqueTraders: raw.uniqueTraders || void 0
|
|
2943
|
-
};
|
|
2944
|
-
}
|
|
2945
|
-
parseRawFill(raw) {
|
|
2946
|
-
const tokenId = raw.asset_id || void 0;
|
|
2947
|
-
const marketConditionId = raw.market || raw.asset_id || "";
|
|
2948
|
-
return {
|
|
2949
|
-
orderId: raw.orderId || raw.order_id || "",
|
|
2950
|
-
tradeId: raw.id || raw.tradeId || "",
|
|
2951
|
-
marketConditionId,
|
|
2952
|
-
outcomeIndex: raw.outcome_index ?? (raw.side === "BUY" ? 0 : 1),
|
|
2953
|
-
side: raw.side === "BUY" || raw.side === "buy" ? "BUY" : "SELL",
|
|
2954
|
-
price: String(raw.price || "0"),
|
|
2955
|
-
size: String(raw.size || "0"),
|
|
2956
|
-
fee: String(raw.fee || "0"),
|
|
2957
|
-
timestamp: raw.timestamp || raw.created_at ? new Date(raw.created_at).getTime() : Date.now(),
|
|
2958
|
-
isMaker: raw.maker_order || raw.is_maker || false,
|
|
2959
|
-
tokenId
|
|
2960
|
-
};
|
|
2961
|
-
}
|
|
2962
|
-
};
|
|
2963
|
-
|
|
2964
|
-
// src/prediction/order-manager.ts
|
|
2965
|
-
var PolymarketOrderManager = class {
|
|
2966
|
-
client;
|
|
2967
|
-
config;
|
|
2968
|
-
seenFillIds = /* @__PURE__ */ new Set();
|
|
2969
|
-
recentFills = [];
|
|
2970
|
-
positions = /* @__PURE__ */ new Map();
|
|
2971
|
-
lastPriceRefresh = 0;
|
|
2972
|
-
PRICE_REFRESH_MS = 1e4;
|
|
2973
|
-
constructor(client, config) {
|
|
2974
|
-
this.client = client;
|
|
2975
|
-
this.config = config;
|
|
2976
|
-
}
|
|
2977
|
-
// ── SIGNAL EXECUTION ───────────────────────────────────────
|
|
2978
|
-
async executeSignal(signal) {
|
|
2979
|
-
if (signal.action === "hold") {
|
|
2980
|
-
return { success: true, status: "cancelled", error: "Hold signal \u2014 no action" };
|
|
2981
|
-
}
|
|
2982
|
-
const violation = this.checkRiskLimits(signal);
|
|
2983
|
-
if (violation) {
|
|
2984
|
-
return { success: false, status: "error", error: violation };
|
|
2985
|
-
}
|
|
2986
|
-
try {
|
|
2987
|
-
const market = await this.client.getMarketByConditionId(signal.marketConditionId);
|
|
2988
|
-
if (!market) {
|
|
2989
|
-
return { success: false, status: "error", error: `Market not found: ${signal.marketConditionId}` };
|
|
2990
|
-
}
|
|
2991
|
-
if (!market.active) {
|
|
2992
|
-
return { success: false, status: "error", error: `Market is closed: ${market.question}` };
|
|
2993
|
-
}
|
|
2994
|
-
const tokenId = market.outcomeTokenIds[signal.outcomeIndex];
|
|
2995
|
-
if (!tokenId) {
|
|
2996
|
-
return { success: false, status: "error", error: `Invalid outcome index: ${signal.outcomeIndex}` };
|
|
2997
|
-
}
|
|
2998
|
-
const isBuy = signal.action === "buy_yes" || signal.action === "buy_no";
|
|
2999
|
-
const side = isBuy ? "BUY" : "SELL";
|
|
3000
|
-
let result;
|
|
3001
|
-
if (signal.orderType === "market") {
|
|
3002
|
-
result = await this.client.placeMarketOrder({
|
|
3003
|
-
tokenId,
|
|
3004
|
-
amount: signal.amount,
|
|
3005
|
-
side
|
|
3006
|
-
});
|
|
3007
|
-
} else {
|
|
3008
|
-
result = await this.client.placeLimitOrder({
|
|
3009
|
-
tokenId,
|
|
3010
|
-
price: signal.limitPrice,
|
|
3011
|
-
size: signal.amount,
|
|
3012
|
-
side
|
|
3013
|
-
});
|
|
3014
|
-
}
|
|
3015
|
-
if (result.success) {
|
|
3016
|
-
console.log(`[prediction] Order placed: ${signal.action} ${signal.amount} on "${market.question}" @ ${signal.limitPrice}`);
|
|
3017
|
-
return { success: true, orderId: result.orderId, status: "resting" };
|
|
3018
|
-
}
|
|
3019
|
-
return { success: false, status: "error", error: "Order placement failed" };
|
|
3020
|
-
} catch (error) {
|
|
3021
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3022
|
-
console.error("[prediction] Order execution error:", message);
|
|
3023
|
-
return { success: false, status: "error", error: message };
|
|
3024
|
-
}
|
|
3025
|
-
}
|
|
3026
|
-
// ── FILL POLLING ───────────────────────────────────────────
|
|
3027
|
-
async pollNewFills() {
|
|
3028
|
-
try {
|
|
3029
|
-
const allFills = await this.client.getTradeHistory();
|
|
3030
|
-
const newFills = [];
|
|
3031
|
-
for (const fill of allFills) {
|
|
3032
|
-
if (!this.seenFillIds.has(fill.tradeId)) {
|
|
3033
|
-
this.seenFillIds.add(fill.tradeId);
|
|
3034
|
-
newFills.push(fill);
|
|
3035
|
-
this.processFill(fill);
|
|
3036
|
-
this.recentFills.push(fill);
|
|
3037
|
-
}
|
|
3038
|
-
}
|
|
3039
|
-
if (this.recentFills.length > 100) {
|
|
3040
|
-
this.recentFills = this.recentFills.slice(-100);
|
|
3041
|
-
}
|
|
3042
|
-
return newFills;
|
|
3043
|
-
} catch (error) {
|
|
3044
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3045
|
-
console.warn("[prediction] Fill polling error:", message);
|
|
3046
|
-
return [];
|
|
3047
|
-
}
|
|
3048
|
-
}
|
|
3049
|
-
getRecentFills() {
|
|
3050
|
-
return this.recentFills;
|
|
3051
|
-
}
|
|
3052
|
-
// ── POSITION QUERIES ───────────────────────────────────────
|
|
3053
|
-
async getPositions(forceRefresh = false) {
|
|
3054
|
-
if (forceRefresh || Date.now() - this.lastPriceRefresh > this.PRICE_REFRESH_MS) {
|
|
3055
|
-
await this.refreshPrices();
|
|
3056
|
-
}
|
|
3057
|
-
return Array.from(this.positions.values()).filter((p) => p.balance > 0).map((p) => this.toExternalPosition(p));
|
|
3058
|
-
}
|
|
3059
|
-
async getAccountSummary() {
|
|
3060
|
-
const positions = await this.getPositions();
|
|
3061
|
-
const totalExposure = positions.reduce((sum, p) => sum + p.costBasis, 0);
|
|
3062
|
-
const totalUnrealizedPnl = positions.reduce((sum, p) => sum + p.unrealizedPnl, 0);
|
|
3063
|
-
const openMarkets = new Set(positions.map((p) => p.marketConditionId));
|
|
3064
|
-
return {
|
|
3065
|
-
totalExposure,
|
|
3066
|
-
totalUnrealizedPnl,
|
|
3067
|
-
openMarketCount: openMarkets.size,
|
|
3068
|
-
openPositionCount: positions.length
|
|
3069
|
-
};
|
|
3070
|
-
}
|
|
3071
|
-
getTotalExposure() {
|
|
3072
|
-
let total = 0;
|
|
3073
|
-
for (const pos of this.positions.values()) {
|
|
3074
|
-
if (pos.balance > 0) total += pos.totalCostBasis;
|
|
3075
|
-
}
|
|
3076
|
-
return total;
|
|
3077
|
-
}
|
|
3078
|
-
// ── CANCELLATION ───────────────────────────────────────────
|
|
3079
|
-
async cancelOrder(orderId) {
|
|
3080
|
-
return this.client.cancelOrder(orderId);
|
|
3081
|
-
}
|
|
3082
|
-
async cancelAllOrders() {
|
|
3083
|
-
return this.client.cancelAllOrders();
|
|
3084
|
-
}
|
|
3085
|
-
// ── PRIVATE ────────────────────────────────────────────────
|
|
3086
|
-
checkRiskLimits(signal) {
|
|
3087
|
-
if (signal.amount > this.config.maxNotionalUSD) {
|
|
3088
|
-
return `Trade exceeds max notional: $${signal.amount} > $${this.config.maxNotionalUSD}`;
|
|
3089
|
-
}
|
|
3090
|
-
if (signal.limitPrice <= 0 || signal.limitPrice >= 1) {
|
|
3091
|
-
return `Invalid price: ${signal.limitPrice} (must be 0.01-0.99)`;
|
|
3092
|
-
}
|
|
3093
|
-
if (signal.confidence < 0.3) {
|
|
3094
|
-
return `Low confidence: ${signal.confidence} < 0.3`;
|
|
3095
|
-
}
|
|
3096
|
-
const isBuy = signal.action === "buy_yes" || signal.action === "buy_no";
|
|
3097
|
-
if (isBuy) {
|
|
3098
|
-
const currentExposure = this.getTotalExposure();
|
|
3099
|
-
if (currentExposure + signal.amount > this.config.maxTotalExposureUSD) {
|
|
3100
|
-
return `Total exposure exceeded: $${currentExposure + signal.amount} > $${this.config.maxTotalExposureUSD}`;
|
|
3101
|
-
}
|
|
3102
|
-
}
|
|
3103
|
-
return null;
|
|
3104
|
-
}
|
|
3105
|
-
processFill(fill) {
|
|
3106
|
-
const key = `${fill.marketConditionId}:${fill.outcomeIndex}`;
|
|
3107
|
-
let pos = this.positions.get(key);
|
|
3108
|
-
const price = parseFloat(fill.price);
|
|
3109
|
-
const size = parseFloat(fill.size);
|
|
3110
|
-
if (!pos) {
|
|
3111
|
-
pos = {
|
|
3112
|
-
marketConditionId: fill.marketConditionId,
|
|
3113
|
-
outcomeIndex: fill.outcomeIndex,
|
|
3114
|
-
marketQuestion: fill.marketQuestion || "",
|
|
3115
|
-
tokenId: fill.tokenId || "",
|
|
3116
|
-
balance: 0,
|
|
3117
|
-
totalCostBasis: 0,
|
|
3118
|
-
totalProceeds: 0,
|
|
3119
|
-
averageEntryPrice: 0,
|
|
3120
|
-
currentPrice: price,
|
|
3121
|
-
category: void 0,
|
|
3122
|
-
endDate: void 0
|
|
3123
|
-
};
|
|
3124
|
-
this.positions.set(key, pos);
|
|
3125
|
-
} else if (!pos.tokenId && fill.tokenId) {
|
|
3126
|
-
pos.tokenId = fill.tokenId;
|
|
3127
|
-
}
|
|
3128
|
-
if (fill.side === "BUY") {
|
|
3129
|
-
const oldCost = pos.averageEntryPrice * pos.balance;
|
|
3130
|
-
const newCost = price * size;
|
|
3131
|
-
pos.balance += size;
|
|
3132
|
-
pos.totalCostBasis += newCost;
|
|
3133
|
-
pos.averageEntryPrice = pos.balance > 0 ? (oldCost + newCost) / pos.balance : 0;
|
|
3134
|
-
} else {
|
|
3135
|
-
pos.balance -= size;
|
|
3136
|
-
pos.totalProceeds += price * size;
|
|
3137
|
-
if (pos.balance < 0) pos.balance = 0;
|
|
3138
|
-
}
|
|
3139
|
-
}
|
|
3140
|
-
async refreshPrices() {
|
|
3141
|
-
const openPositions = Array.from(this.positions.values()).filter((p) => p.balance > 0);
|
|
3142
|
-
await Promise.all(
|
|
3143
|
-
openPositions.map(async (pos) => {
|
|
3144
|
-
try {
|
|
3145
|
-
if (!pos.tokenId) {
|
|
3146
|
-
const market = await this.client.getMarketByConditionId(pos.marketConditionId);
|
|
3147
|
-
if (market && market.outcomeTokenIds[pos.outcomeIndex]) {
|
|
3148
|
-
pos.tokenId = market.outcomeTokenIds[pos.outcomeIndex];
|
|
3149
|
-
}
|
|
3150
|
-
}
|
|
3151
|
-
if (pos.tokenId) {
|
|
3152
|
-
const price = await this.client.getMidpointPrice(pos.tokenId);
|
|
3153
|
-
if (price > 0) pos.currentPrice = price;
|
|
3154
|
-
}
|
|
3155
|
-
} catch {
|
|
3156
|
-
}
|
|
3157
|
-
})
|
|
3158
|
-
);
|
|
3159
|
-
this.lastPriceRefresh = Date.now();
|
|
3160
|
-
}
|
|
3161
|
-
toExternalPosition(pos) {
|
|
3162
|
-
const unrealizedPnl = pos.balance > 0 ? (pos.currentPrice - pos.averageEntryPrice) * pos.balance : 0;
|
|
3163
|
-
return {
|
|
3164
|
-
marketConditionId: pos.marketConditionId,
|
|
3165
|
-
marketQuestion: pos.marketQuestion,
|
|
3166
|
-
outcomeIndex: pos.outcomeIndex,
|
|
3167
|
-
outcomeLabel: pos.outcomeIndex === 0 ? "Yes" : "No",
|
|
3168
|
-
tokenId: pos.tokenId,
|
|
3169
|
-
balance: pos.balance,
|
|
3170
|
-
averageEntryPrice: pos.averageEntryPrice,
|
|
3171
|
-
currentPrice: pos.currentPrice,
|
|
3172
|
-
unrealizedPnl,
|
|
3173
|
-
costBasis: pos.totalCostBasis - pos.totalProceeds,
|
|
3174
|
-
endDate: pos.endDate,
|
|
3175
|
-
category: pos.category
|
|
3176
|
-
};
|
|
3177
|
-
}
|
|
3178
|
-
};
|
|
3179
|
-
|
|
3180
|
-
// src/spot/client.ts
|
|
3181
|
-
import {
|
|
3182
|
-
createPublicClient,
|
|
3183
|
-
createWalletClient,
|
|
3184
|
-
http,
|
|
3185
|
-
maxUint256
|
|
3186
|
-
} from "viem";
|
|
3187
|
-
import { privateKeyToAccount } from "viem/accounts";
|
|
3188
|
-
var SpotDEXClient = class {
|
|
3189
|
-
account;
|
|
3190
|
-
clients = /* @__PURE__ */ new Map();
|
|
3191
|
-
decimalsCache = /* @__PURE__ */ new Map();
|
|
3192
|
-
symbolCache = /* @__PURE__ */ new Map();
|
|
3193
|
-
constructor(privateKey) {
|
|
3194
|
-
this.account = privateKeyToAccount(privateKey);
|
|
3195
|
-
}
|
|
3196
|
-
get address() {
|
|
3197
|
-
return this.account.address;
|
|
3198
|
-
}
|
|
3199
|
-
getClients(chainName) {
|
|
3200
|
-
const cached = this.clients.get(chainName);
|
|
3201
|
-
if (cached) return cached;
|
|
3202
|
-
const chainConfig = getChainConfig(chainName);
|
|
3203
|
-
if (!chainConfig) {
|
|
3204
|
-
throw new Error(`Unknown chain: ${chainName}`);
|
|
3205
|
-
}
|
|
3206
|
-
const publicClient = createPublicClient({
|
|
3207
|
-
chain: chainConfig.viemChain,
|
|
3208
|
-
transport: http(chainConfig.rpcUrl)
|
|
3209
|
-
});
|
|
3210
|
-
const walletClient = createWalletClient({
|
|
3211
|
-
account: this.account,
|
|
3212
|
-
chain: chainConfig.viemChain,
|
|
3213
|
-
transport: http(chainConfig.rpcUrl)
|
|
3214
|
-
});
|
|
3215
|
-
const entry = { publicClient, walletClient, chainConfig };
|
|
3216
|
-
this.clients.set(chainName, entry);
|
|
3217
|
-
return entry;
|
|
3218
|
-
}
|
|
3219
|
-
async getDecimals(token, chainName) {
|
|
3220
|
-
const key = `${chainName}:${token}`;
|
|
3221
|
-
const cached = this.decimalsCache.get(key);
|
|
3222
|
-
if (cached !== void 0) return cached;
|
|
3223
|
-
const { publicClient } = this.getClients(chainName);
|
|
3224
|
-
const decimals = await publicClient.readContract({
|
|
3225
|
-
address: token,
|
|
3226
|
-
abi: ERC20_ABI,
|
|
3227
|
-
functionName: "decimals"
|
|
3228
|
-
});
|
|
3229
|
-
this.decimalsCache.set(key, decimals);
|
|
3230
|
-
return decimals;
|
|
3231
|
-
}
|
|
3232
|
-
async getSymbol(token, chainName) {
|
|
3233
|
-
const key = `${chainName}:${token}`;
|
|
3234
|
-
const cached = this.symbolCache.get(key);
|
|
3235
|
-
if (cached) return cached;
|
|
3236
|
-
const { publicClient } = this.getClients(chainName);
|
|
3237
|
-
const symbol = await publicClient.readContract({
|
|
3238
|
-
address: token,
|
|
3239
|
-
abi: ERC20_ABI,
|
|
3240
|
-
functionName: "symbol"
|
|
3241
|
-
});
|
|
3242
|
-
this.symbolCache.set(key, symbol);
|
|
3243
|
-
return symbol;
|
|
3244
|
-
}
|
|
3245
|
-
async getBalance(token, chainName) {
|
|
3246
|
-
const { publicClient } = this.getClients(chainName);
|
|
3247
|
-
return await publicClient.readContract({
|
|
3248
|
-
address: token,
|
|
3249
|
-
abi: ERC20_ABI,
|
|
3250
|
-
functionName: "balanceOf",
|
|
3251
|
-
args: [this.account.address]
|
|
3252
|
-
});
|
|
3253
|
-
}
|
|
3254
|
-
/** Get the native token balance (ETH, POL, etc.) on a chain */
|
|
3255
|
-
async getNativeBalance(chainName) {
|
|
3256
|
-
const { publicClient } = this.getClients(chainName);
|
|
3257
|
-
return await publicClient.getBalance({ address: this.account.address });
|
|
3258
|
-
}
|
|
3259
|
-
async ensureApproval(token, spender, amount, chainName) {
|
|
3260
|
-
const { publicClient, walletClient } = this.getClients(chainName);
|
|
3261
|
-
const allowance = await publicClient.readContract({
|
|
3262
|
-
address: token,
|
|
3263
|
-
abi: ERC20_ABI,
|
|
3264
|
-
functionName: "allowance",
|
|
3265
|
-
args: [this.account.address, spender]
|
|
3266
|
-
});
|
|
3267
|
-
if (allowance >= amount) return null;
|
|
3268
|
-
const hash = await walletClient.writeContract({
|
|
3269
|
-
address: token,
|
|
3270
|
-
abi: ERC20_ABI,
|
|
3271
|
-
functionName: "approve",
|
|
3272
|
-
args: [spender, maxUint256]
|
|
3273
|
-
});
|
|
3274
|
-
await publicClient.waitForTransactionReceipt({ hash });
|
|
3275
|
-
console.log(`[spot] Approved ${token} for ${spender} on ${chainName} (tx: ${hash})`);
|
|
3276
|
-
return hash;
|
|
3277
|
-
}
|
|
3278
|
-
};
|
|
3279
|
-
|
|
3280
|
-
// src/spot/uniswap.ts
|
|
3281
|
-
var FEE_TIERS = [500, 3e3, 1e4];
|
|
3282
|
-
var UniswapAdapter = class {
|
|
3283
|
-
client;
|
|
3284
|
-
constructor(client) {
|
|
3285
|
-
this.client = client;
|
|
3286
|
-
}
|
|
3287
|
-
async quote(params) {
|
|
3288
|
-
const chainConfig = getChainConfig(params.chain);
|
|
3289
|
-
if (!chainConfig?.uniswapQuoter) {
|
|
3290
|
-
throw new Error(`No Uniswap QuoterV2 on ${params.chain}`);
|
|
3291
|
-
}
|
|
3292
|
-
const { publicClient } = this.client.getClients(params.chain);
|
|
3293
|
-
let bestAmountOut = 0n;
|
|
3294
|
-
let bestFeeTier = 0;
|
|
3295
|
-
for (const fee of FEE_TIERS) {
|
|
3296
|
-
try {
|
|
3297
|
-
const result = await publicClient.simulateContract({
|
|
3298
|
-
address: chainConfig.uniswapQuoter,
|
|
3299
|
-
abi: UNISWAP_QUOTER_V2_ABI,
|
|
3300
|
-
functionName: "quoteExactInputSingle",
|
|
3301
|
-
args: [
|
|
3302
|
-
{
|
|
3303
|
-
tokenIn: params.tokenIn,
|
|
3304
|
-
tokenOut: params.tokenOut,
|
|
3305
|
-
amountIn: params.amountIn,
|
|
3306
|
-
fee,
|
|
3307
|
-
sqrtPriceLimitX96: 0n
|
|
3308
|
-
}
|
|
3309
|
-
]
|
|
3310
|
-
});
|
|
3311
|
-
const amountOut = result.result[0];
|
|
3312
|
-
if (amountOut > bestAmountOut) {
|
|
3313
|
-
bestAmountOut = amountOut;
|
|
3314
|
-
bestFeeTier = fee;
|
|
3315
|
-
}
|
|
3316
|
-
} catch {
|
|
3317
|
-
}
|
|
3318
|
-
}
|
|
3319
|
-
if (bestAmountOut === 0n) {
|
|
3320
|
-
throw new Error(`No Uniswap V3 pool found for ${params.tokenIn}/${params.tokenOut} on ${params.chain}`);
|
|
3321
|
-
}
|
|
3322
|
-
const tokenInSymbol = await this.client.getSymbol(params.tokenIn, params.chain);
|
|
3323
|
-
const tokenOutSymbol = await this.client.getSymbol(params.tokenOut, params.chain);
|
|
3324
|
-
return {
|
|
3325
|
-
amountOut: bestAmountOut,
|
|
3326
|
-
feeTier: bestFeeTier,
|
|
3327
|
-
route: `${tokenInSymbol} \u2192 ${tokenOutSymbol} (${bestFeeTier / 1e4}%)`
|
|
3328
|
-
};
|
|
3329
|
-
}
|
|
3330
|
-
async swap(params) {
|
|
3331
|
-
const chainConfig = getChainConfig(params.chain);
|
|
3332
|
-
if (!chainConfig) {
|
|
3333
|
-
throw new Error(`Unknown chain: ${params.chain}`);
|
|
3334
|
-
}
|
|
3335
|
-
const routerAddress = chainConfig.dexRouters.uniswap;
|
|
3336
|
-
if (!routerAddress) {
|
|
3337
|
-
throw new Error(`No Uniswap router on ${params.chain}`);
|
|
3338
|
-
}
|
|
3339
|
-
const quoteResult = await this.quote(params);
|
|
3340
|
-
const amountOutMinimum = quoteResult.amountOut * BigInt(1e4 - params.slippageBps) / 10000n;
|
|
3341
|
-
await this.client.ensureApproval(params.tokenIn, routerAddress, params.amountIn, params.chain);
|
|
3342
|
-
const { publicClient, walletClient } = this.client.getClients(params.chain);
|
|
3343
|
-
const recipient = params.recipient ?? this.client.address;
|
|
3344
|
-
try {
|
|
3345
|
-
const hash = await walletClient.writeContract({
|
|
3346
|
-
address: routerAddress,
|
|
3347
|
-
abi: UNISWAP_SWAP_ROUTER_ABI,
|
|
3348
|
-
functionName: "exactInputSingle",
|
|
3349
|
-
args: [
|
|
3350
|
-
{
|
|
3351
|
-
tokenIn: params.tokenIn,
|
|
3352
|
-
tokenOut: params.tokenOut,
|
|
3353
|
-
fee: quoteResult.feeTier,
|
|
3354
|
-
recipient,
|
|
3355
|
-
amountIn: params.amountIn,
|
|
3356
|
-
amountOutMinimum,
|
|
3357
|
-
sqrtPriceLimitX96: 0n
|
|
3358
|
-
}
|
|
3359
|
-
]
|
|
3360
|
-
});
|
|
3361
|
-
const receipt = await publicClient.waitForTransactionReceipt({ hash });
|
|
3362
|
-
const tokenInDecimals = await this.client.getDecimals(params.tokenIn, params.chain);
|
|
3363
|
-
const tokenOutDecimals = await this.client.getDecimals(params.tokenOut, params.chain);
|
|
3364
|
-
const amountInFloat = Number(params.amountIn) / 10 ** tokenInDecimals;
|
|
3365
|
-
const amountOutFloat = Number(quoteResult.amountOut) / 10 ** tokenOutDecimals;
|
|
3366
|
-
const effectivePrice = amountOutFloat / amountInFloat;
|
|
3367
|
-
const gasUsed = receipt.gasUsed ?? 0n;
|
|
3368
|
-
const gasPrice = receipt.effectiveGasPrice ?? 0n;
|
|
3369
|
-
return {
|
|
3370
|
-
success: true,
|
|
3371
|
-
txHash: hash,
|
|
3372
|
-
amountIn: params.amountIn,
|
|
3373
|
-
amountOut: quoteResult.amountOut,
|
|
3374
|
-
tokenIn: params.tokenIn,
|
|
3375
|
-
tokenOut: params.tokenOut,
|
|
3376
|
-
effectivePrice,
|
|
3377
|
-
gasCost: gasUsed * gasPrice,
|
|
3378
|
-
chain: params.chain,
|
|
3379
|
-
dex: "uniswap"
|
|
3380
|
-
};
|
|
3381
|
-
} catch (err) {
|
|
3382
|
-
return {
|
|
3383
|
-
success: false,
|
|
3384
|
-
txHash: "0x0",
|
|
3385
|
-
amountIn: params.amountIn,
|
|
3386
|
-
amountOut: 0n,
|
|
3387
|
-
tokenIn: params.tokenIn,
|
|
3388
|
-
tokenOut: params.tokenOut,
|
|
3389
|
-
effectivePrice: 0,
|
|
3390
|
-
gasCost: 0n,
|
|
3391
|
-
chain: params.chain,
|
|
3392
|
-
dex: "uniswap",
|
|
3393
|
-
error: err.message
|
|
3394
|
-
};
|
|
3395
|
-
}
|
|
3396
|
-
}
|
|
3397
|
-
};
|
|
3398
|
-
|
|
3399
|
-
// src/spot/aerodrome.ts
|
|
3400
|
-
var AerodromeAdapter = class {
|
|
3401
|
-
client;
|
|
3402
|
-
constructor(client) {
|
|
3403
|
-
this.client = client;
|
|
3404
|
-
}
|
|
3405
|
-
async quote(params) {
|
|
3406
|
-
if (params.chain !== "base") {
|
|
3407
|
-
throw new Error("Aerodrome is only available on Base");
|
|
3408
|
-
}
|
|
3409
|
-
const chainConfig = getChainConfig("base");
|
|
3410
|
-
const routerAddress = chainConfig.dexRouters.aerodrome;
|
|
3411
|
-
if (!routerAddress) {
|
|
3412
|
-
throw new Error("No Aerodrome router configured for Base");
|
|
3413
|
-
}
|
|
3414
|
-
const { publicClient } = this.client.getClients("base");
|
|
3415
|
-
let bestAmountOut = 0n;
|
|
3416
|
-
let bestStable = false;
|
|
3417
|
-
for (const stable of [true, false]) {
|
|
3418
|
-
try {
|
|
3419
|
-
const route = [
|
|
3420
|
-
{
|
|
3421
|
-
from: params.tokenIn,
|
|
3422
|
-
to: params.tokenOut,
|
|
3423
|
-
stable,
|
|
3424
|
-
factory: AERODROME_DEFAULT_FACTORY
|
|
3425
|
-
}
|
|
3426
|
-
];
|
|
3427
|
-
const amounts = await publicClient.readContract({
|
|
3428
|
-
address: routerAddress,
|
|
3429
|
-
abi: AERODROME_ROUTER_ABI,
|
|
3430
|
-
functionName: "getAmountsOut",
|
|
3431
|
-
args: [params.amountIn, route]
|
|
3432
|
-
});
|
|
3433
|
-
const amountOut = amounts[amounts.length - 1];
|
|
3434
|
-
if (amountOut > bestAmountOut) {
|
|
3435
|
-
bestAmountOut = amountOut;
|
|
3436
|
-
bestStable = stable;
|
|
3437
|
-
}
|
|
3438
|
-
} catch {
|
|
3439
|
-
}
|
|
3440
|
-
}
|
|
3441
|
-
if (bestAmountOut === 0n) {
|
|
3442
|
-
throw new Error(`No Aerodrome pool found for ${params.tokenIn}/${params.tokenOut}`);
|
|
3443
|
-
}
|
|
3444
|
-
const tokenInSymbol = await this.client.getSymbol(params.tokenIn, "base");
|
|
3445
|
-
const tokenOutSymbol = await this.client.getSymbol(params.tokenOut, "base");
|
|
3446
|
-
return {
|
|
3447
|
-
amountOut: bestAmountOut,
|
|
3448
|
-
stable: bestStable,
|
|
3449
|
-
route: `${tokenInSymbol} \u2192 ${tokenOutSymbol} (${bestStable ? "stable" : "volatile"})`
|
|
3450
|
-
};
|
|
3451
|
-
}
|
|
3452
|
-
async swap(params) {
|
|
3453
|
-
if (params.chain !== "base") {
|
|
3454
|
-
throw new Error("Aerodrome is only available on Base");
|
|
3455
|
-
}
|
|
3456
|
-
const chainConfig = getChainConfig("base");
|
|
3457
|
-
const routerAddress = chainConfig.dexRouters.aerodrome;
|
|
3458
|
-
if (!routerAddress) {
|
|
3459
|
-
throw new Error("No Aerodrome router configured for Base");
|
|
3460
|
-
}
|
|
3461
|
-
const quoteResult = await this.quote(params);
|
|
3462
|
-
const amountOutMin = quoteResult.amountOut * BigInt(1e4 - params.slippageBps) / 10000n;
|
|
3463
|
-
await this.client.ensureApproval(params.tokenIn, routerAddress, params.amountIn, "base");
|
|
3464
|
-
const { publicClient, walletClient } = this.client.getClients("base");
|
|
3465
|
-
const recipient = params.recipient ?? this.client.address;
|
|
3466
|
-
const route = [
|
|
3467
|
-
{
|
|
3468
|
-
from: params.tokenIn,
|
|
3469
|
-
to: params.tokenOut,
|
|
3470
|
-
stable: quoteResult.stable,
|
|
3471
|
-
factory: AERODROME_DEFAULT_FACTORY
|
|
3472
|
-
}
|
|
3473
|
-
];
|
|
3474
|
-
const deadline = BigInt(Math.floor(Date.now() / 1e3) + 300);
|
|
3475
|
-
try {
|
|
3476
|
-
const hash = await walletClient.writeContract({
|
|
3477
|
-
address: routerAddress,
|
|
3478
|
-
abi: AERODROME_ROUTER_ABI,
|
|
3479
|
-
functionName: "swapExactTokensForTokens",
|
|
3480
|
-
args: [params.amountIn, amountOutMin, route, recipient, deadline]
|
|
3481
|
-
});
|
|
3482
|
-
const receipt = await publicClient.waitForTransactionReceipt({ hash });
|
|
3483
|
-
const tokenInDecimals = await this.client.getDecimals(params.tokenIn, "base");
|
|
3484
|
-
const tokenOutDecimals = await this.client.getDecimals(params.tokenOut, "base");
|
|
3485
|
-
const amountInFloat = Number(params.amountIn) / 10 ** tokenInDecimals;
|
|
3486
|
-
const amountOutFloat = Number(quoteResult.amountOut) / 10 ** tokenOutDecimals;
|
|
3487
|
-
const effectivePrice = amountOutFloat / amountInFloat;
|
|
3488
|
-
const gasUsed = receipt.gasUsed ?? 0n;
|
|
3489
|
-
const gasPrice = receipt.effectiveGasPrice ?? 0n;
|
|
3490
|
-
return {
|
|
3491
|
-
success: true,
|
|
3492
|
-
txHash: hash,
|
|
3493
|
-
amountIn: params.amountIn,
|
|
3494
|
-
amountOut: quoteResult.amountOut,
|
|
3495
|
-
tokenIn: params.tokenIn,
|
|
3496
|
-
tokenOut: params.tokenOut,
|
|
3497
|
-
effectivePrice,
|
|
3498
|
-
gasCost: gasUsed * gasPrice,
|
|
3499
|
-
chain: "base",
|
|
3500
|
-
dex: "aerodrome"
|
|
3501
|
-
};
|
|
3502
|
-
} catch (err) {
|
|
3503
|
-
return {
|
|
3504
|
-
success: false,
|
|
3505
|
-
txHash: "0x0",
|
|
3506
|
-
amountIn: params.amountIn,
|
|
3507
|
-
amountOut: 0n,
|
|
3508
|
-
tokenIn: params.tokenIn,
|
|
3509
|
-
tokenOut: params.tokenOut,
|
|
3510
|
-
effectivePrice: 0,
|
|
3511
|
-
gasCost: 0n,
|
|
3512
|
-
chain: "base",
|
|
3513
|
-
dex: "aerodrome",
|
|
3514
|
-
error: err.message
|
|
3515
|
-
};
|
|
3516
|
-
}
|
|
3517
|
-
}
|
|
3518
|
-
};
|
|
3519
|
-
|
|
3520
|
-
// src/spot/swap-manager.ts
|
|
3521
|
-
var TOKEN_ALIASES = {
|
|
3522
|
-
base: {
|
|
3523
|
-
ETH: "0x4200000000000000000000000000000000000006",
|
|
3524
|
-
WETH: "0x4200000000000000000000000000000000000006",
|
|
3525
|
-
USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
3526
|
-
USDbC: "0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA",
|
|
3527
|
-
DAI: "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb"
|
|
3528
|
-
},
|
|
3529
|
-
ethereum: {
|
|
3530
|
-
ETH: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
|
3531
|
-
WETH: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
|
3532
|
-
USDC: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
|
3533
|
-
USDT: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
|
3534
|
-
DAI: "0x6B175474E89094C44Da98b954EedeAC495271d0F"
|
|
3535
|
-
},
|
|
3536
|
-
arbitrum: {
|
|
3537
|
-
ETH: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
|
|
3538
|
-
WETH: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
|
|
3539
|
-
USDC: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
|
|
3540
|
-
"USDC.e": "0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8",
|
|
3541
|
-
USDT: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9"
|
|
3542
|
-
},
|
|
3543
|
-
polygon: {
|
|
3544
|
-
POL: "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270",
|
|
3545
|
-
WPOL: "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270",
|
|
3546
|
-
WMATIC: "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270",
|
|
3547
|
-
USDC: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
|
|
3548
|
-
"USDC.e": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",
|
|
3549
|
-
USDT: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F"
|
|
3550
|
-
}
|
|
3551
|
-
};
|
|
3552
|
-
var SpotSwapManager = class {
|
|
3553
|
-
dexClient;
|
|
3554
|
-
uniswap;
|
|
3555
|
-
aerodrome;
|
|
3556
|
-
config;
|
|
3557
|
-
constructor(privateKey, config) {
|
|
3558
|
-
this.config = config;
|
|
3559
|
-
this.dexClient = new SpotDEXClient(privateKey);
|
|
3560
|
-
this.uniswap = new UniswapAdapter(this.dexClient);
|
|
3561
|
-
this.aerodrome = new AerodromeAdapter(this.dexClient);
|
|
3562
|
-
}
|
|
3563
|
-
get client() {
|
|
3564
|
-
return this.dexClient;
|
|
3565
|
-
}
|
|
3566
|
-
/** Resolve a token symbol or address to a hex address */
|
|
3567
|
-
resolveToken(tokenOrSymbol, chain) {
|
|
3568
|
-
if (tokenOrSymbol.startsWith("0x") && tokenOrSymbol.length === 42) {
|
|
3569
|
-
return tokenOrSymbol;
|
|
3570
|
-
}
|
|
3571
|
-
const chainAliases = TOKEN_ALIASES[chain];
|
|
3572
|
-
if (chainAliases) {
|
|
3573
|
-
const upper = tokenOrSymbol.toUpperCase();
|
|
3574
|
-
const address = chainAliases[upper] ?? chainAliases[tokenOrSymbol];
|
|
3575
|
-
if (address) return address;
|
|
3576
|
-
}
|
|
3577
|
-
const chainConfig = getChainConfig(chain);
|
|
3578
|
-
if (chainConfig) {
|
|
3579
|
-
const upper = tokenOrSymbol.toUpperCase();
|
|
3580
|
-
if (upper === "WETH" || upper === "ETH") return chainConfig.wethAddress;
|
|
3581
|
-
if (upper === "USDC") return chainConfig.usdcAddress;
|
|
3582
|
-
}
|
|
3583
|
-
throw new Error(`Cannot resolve token "${tokenOrSymbol}" on ${chain}. Provide contract address.`);
|
|
3584
|
-
}
|
|
3585
|
-
/** Select DEX: explicit choice, or auto-detect */
|
|
3586
|
-
selectDex(chain, dex) {
|
|
3587
|
-
if (dex) return dex;
|
|
3588
|
-
const chainConfig = getChainConfig(chain);
|
|
3589
|
-
if (chain === "base" && chainConfig?.dexRouters.aerodrome) {
|
|
3590
|
-
return "aerodrome";
|
|
3591
|
-
}
|
|
3592
|
-
return "uniswap";
|
|
3593
|
-
}
|
|
3594
|
-
async getQuote(params) {
|
|
3595
|
-
const chain = params.chain ?? this.config.defaultChain;
|
|
3596
|
-
const dex = this.selectDex(chain, params.dex);
|
|
3597
|
-
const swapParams = {
|
|
3598
|
-
tokenIn: this.resolveToken(params.tokenIn, chain),
|
|
3599
|
-
tokenOut: this.resolveToken(params.tokenOut, chain),
|
|
3600
|
-
amountIn: params.amountIn,
|
|
3601
|
-
slippageBps: this.config.maxSlippageBps,
|
|
3602
|
-
chain,
|
|
3603
|
-
dex
|
|
3604
|
-
};
|
|
3605
|
-
if (dex === "aerodrome") {
|
|
3606
|
-
return this.aerodrome.quote(swapParams);
|
|
3607
|
-
}
|
|
3608
|
-
return this.uniswap.quote(swapParams);
|
|
3609
|
-
}
|
|
3610
|
-
async executeSwap(params) {
|
|
3611
|
-
const chain = params.chain ?? this.config.defaultChain;
|
|
3612
|
-
const dex = this.selectDex(chain, params.dex);
|
|
3613
|
-
if (!this.config.chains.includes(chain)) {
|
|
3614
|
-
return {
|
|
3615
|
-
success: false,
|
|
3616
|
-
txHash: "0x0",
|
|
3617
|
-
amountIn: params.amountIn,
|
|
3618
|
-
amountOut: 0n,
|
|
3619
|
-
tokenIn: "0x0",
|
|
3620
|
-
tokenOut: "0x0",
|
|
3621
|
-
effectivePrice: 0,
|
|
3622
|
-
gasCost: 0n,
|
|
3623
|
-
chain,
|
|
3624
|
-
dex,
|
|
3625
|
-
error: `Chain "${chain}" not enabled in spot config (enabled: ${this.config.chains.join(", ")})`
|
|
3626
|
-
};
|
|
3627
|
-
}
|
|
3628
|
-
const MIN_GAS_WEI2 = 100000000000000n;
|
|
3629
|
-
try {
|
|
3630
|
-
const nativeBalance = await this.dexClient.getNativeBalance(chain);
|
|
3631
|
-
if (nativeBalance < MIN_GAS_WEI2) {
|
|
3632
|
-
const chainConfig = getChainConfig(chain);
|
|
3633
|
-
const nativeName = chainConfig?.nativeCurrency ?? "ETH";
|
|
3634
|
-
return {
|
|
3635
|
-
success: false,
|
|
3636
|
-
txHash: "0x0",
|
|
3637
|
-
amountIn: params.amountIn,
|
|
3638
|
-
amountOut: 0n,
|
|
3639
|
-
tokenIn: "0x0",
|
|
3640
|
-
tokenOut: "0x0",
|
|
3641
|
-
effectivePrice: 0,
|
|
3642
|
-
gasCost: 0n,
|
|
3643
|
-
chain,
|
|
3644
|
-
dex,
|
|
3645
|
-
error: `Insufficient ${nativeName} for gas on ${chain}. Balance: ${nativeBalance} wei. Need at least 0.0001 ${nativeName}. Bridge gas to ${chain} before swapping.`
|
|
3646
|
-
};
|
|
3647
|
-
}
|
|
3648
|
-
} catch {
|
|
3649
|
-
}
|
|
3650
|
-
const swapParams = {
|
|
3651
|
-
tokenIn: this.resolveToken(params.tokenIn, chain),
|
|
3652
|
-
tokenOut: this.resolveToken(params.tokenOut, chain),
|
|
3653
|
-
amountIn: params.amountIn,
|
|
3654
|
-
slippageBps: params.slippageBps ?? this.config.maxSlippageBps,
|
|
3655
|
-
chain,
|
|
3656
|
-
dex
|
|
3657
|
-
};
|
|
3658
|
-
console.log(`[spot] Swapping on ${dex}/${chain}: ${params.tokenIn} \u2192 ${params.tokenOut} (${params.amountIn})`);
|
|
3659
|
-
if (dex === "aerodrome") {
|
|
3660
|
-
return this.aerodrome.swap(swapParams);
|
|
3661
|
-
}
|
|
3662
|
-
return this.uniswap.swap(swapParams);
|
|
3663
|
-
}
|
|
3664
|
-
};
|
|
3665
|
-
|
|
3666
|
-
// src/bridge/types.ts
|
|
3667
|
-
var DEFAULT_BRIDGE_CONFIG = {
|
|
3668
|
-
enabled: false,
|
|
3669
|
-
defaultBridge: "across",
|
|
3670
|
-
maxBridgeValueUSD: 1e4,
|
|
3671
|
-
fillTimeoutMs: 3e5,
|
|
3672
|
-
// 5 minutes
|
|
3673
|
-
pollIntervalMs: 2e3
|
|
3674
|
-
// 2 seconds
|
|
3675
|
-
};
|
|
3676
|
-
var ACROSS_SPOKE_POOL_ABI = [
|
|
3677
|
-
{
|
|
3678
|
-
type: "function",
|
|
3679
|
-
name: "depositV3",
|
|
3680
|
-
inputs: [
|
|
3681
|
-
{ name: "depositor", type: "address" },
|
|
3682
|
-
{ name: "recipient", type: "address" },
|
|
3683
|
-
{ name: "inputToken", type: "address" },
|
|
3684
|
-
{ name: "outputToken", type: "address" },
|
|
3685
|
-
{ name: "inputAmount", type: "uint256" },
|
|
3686
|
-
{ name: "outputAmount", type: "uint256" },
|
|
3687
|
-
{ name: "destinationChainId", type: "uint256" },
|
|
3688
|
-
{ name: "exclusiveRelayer", type: "address" },
|
|
3689
|
-
{ name: "quoteTimestamp", type: "uint32" },
|
|
3690
|
-
{ name: "fillDeadline", type: "uint32" },
|
|
3691
|
-
{ name: "exclusivityDeadline", type: "uint32" },
|
|
3692
|
-
{ name: "message", type: "bytes" }
|
|
3693
|
-
],
|
|
3694
|
-
outputs: [],
|
|
3695
|
-
stateMutability: "payable"
|
|
3696
|
-
}
|
|
3697
|
-
];
|
|
3698
|
-
|
|
3699
|
-
// src/bridge/across.ts
|
|
3700
|
-
var ACROSS_API_BASE = "https://app.across.to/api";
|
|
3701
|
-
var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
3702
|
-
var AcrossAdapter = class {
|
|
3703
|
-
client;
|
|
3704
|
-
config;
|
|
3705
|
-
constructor(client, config) {
|
|
3706
|
-
this.client = client;
|
|
3707
|
-
this.config = config;
|
|
3708
|
-
}
|
|
3709
|
-
async estimateFee(request) {
|
|
3710
|
-
const fromChain = getChainConfig(request.fromChain);
|
|
3711
|
-
const toChain = getChainConfig(request.toChain);
|
|
3712
|
-
if (!fromChain) throw new Error(`Unknown source chain: ${request.fromChain}`);
|
|
3713
|
-
if (!toChain) throw new Error(`Unknown destination chain: ${request.toChain}`);
|
|
3714
|
-
const outputToken = this.resolveOutputToken(request.token, request.fromChain, request.toChain);
|
|
3715
|
-
const params = new URLSearchParams({
|
|
3716
|
-
inputToken: request.token,
|
|
3717
|
-
outputToken,
|
|
3718
|
-
originChainId: fromChain.chainId.toString(),
|
|
3719
|
-
destinationChainId: toChain.chainId.toString(),
|
|
3720
|
-
amount: request.amount.toString(),
|
|
3721
|
-
recipient: request.recipient ?? this.client.address
|
|
3722
|
-
});
|
|
3723
|
-
const url = `${ACROSS_API_BASE}/suggested-fees?${params}`;
|
|
3724
|
-
const res = await fetch(url);
|
|
3725
|
-
if (!res.ok) {
|
|
3726
|
-
const body = await res.text();
|
|
3727
|
-
throw new Error(`Across fee API error (${res.status}): ${body}`);
|
|
3728
|
-
}
|
|
3729
|
-
const data = await res.json();
|
|
3730
|
-
const totalFeePct = parseFloat(data.totalRelayFee.pct) / 1e18;
|
|
3731
|
-
const feeAmount = BigInt(Math.floor(Number(request.amount) * totalFeePct));
|
|
3732
|
-
const outputAmount = request.amount - feeAmount;
|
|
3733
|
-
return {
|
|
3734
|
-
totalRelayFeePct: totalFeePct * 100,
|
|
3735
|
-
capitalFeePct: parseFloat(data.relayerCapitalFee.pct) / 1e18 * 100,
|
|
3736
|
-
lpFeePct: parseFloat(data.lpFee.pct) / 1e18 * 100,
|
|
3737
|
-
relayGasFeePct: parseFloat(data.relayerGasFee.pct) / 1e18 * 100,
|
|
3738
|
-
timestamp: parseInt(data.timestamp),
|
|
3739
|
-
estimatedFillTimeSec: data.estimatedFillTimeSec,
|
|
3740
|
-
isAmountTooLow: data.isAmountTooLow,
|
|
3741
|
-
outputAmount
|
|
3742
|
-
};
|
|
3743
|
-
}
|
|
3744
|
-
async deposit(request) {
|
|
3745
|
-
const fromChain = getChainConfig(request.fromChain);
|
|
3746
|
-
const toChain = getChainConfig(request.toChain);
|
|
3747
|
-
if (!fromChain?.acrossSpokePool) throw new Error(`No Across SpokePool on ${request.fromChain}`);
|
|
3748
|
-
if (!toChain) throw new Error(`Unknown destination chain: ${request.toChain}`);
|
|
3749
|
-
const spokePool = fromChain.acrossSpokePool;
|
|
3750
|
-
const outputToken = this.resolveOutputToken(request.token, request.fromChain, request.toChain);
|
|
3751
|
-
const recipient = request.recipient ?? this.client.address;
|
|
3752
|
-
await this.client.ensureApproval(request.token, spokePool, request.amount, request.fromChain);
|
|
3753
|
-
const feeEstimate = await this.estimateFee(request);
|
|
3754
|
-
if (feeEstimate.isAmountTooLow) {
|
|
3755
|
-
return {
|
|
3756
|
-
success: false,
|
|
3757
|
-
depositTxHash: "0x0",
|
|
3758
|
-
fromChain: request.fromChain,
|
|
3759
|
-
toChain: request.toChain,
|
|
3760
|
-
token: request.token,
|
|
3761
|
-
amount: request.amount,
|
|
3762
|
-
fee: 0n,
|
|
3763
|
-
error: "Amount too low for Across bridge"
|
|
3764
|
-
};
|
|
3765
|
-
}
|
|
3766
|
-
const { publicClient, walletClient } = this.client.getClients(request.fromChain);
|
|
3767
|
-
const fillDeadline = Math.floor(Date.now() / 1e3) + 21600;
|
|
3768
|
-
try {
|
|
3769
|
-
const hash = await walletClient.writeContract({
|
|
3770
|
-
address: spokePool,
|
|
3771
|
-
abi: ACROSS_SPOKE_POOL_ABI,
|
|
3772
|
-
functionName: "depositV3",
|
|
3773
|
-
args: [
|
|
3774
|
-
this.client.address,
|
|
3775
|
-
// depositor
|
|
3776
|
-
recipient,
|
|
3777
|
-
// recipient
|
|
3778
|
-
request.token,
|
|
3779
|
-
// inputToken
|
|
3780
|
-
outputToken,
|
|
3781
|
-
// outputToken
|
|
3782
|
-
request.amount,
|
|
3783
|
-
// inputAmount
|
|
3784
|
-
feeEstimate.outputAmount,
|
|
3785
|
-
// outputAmount (inputAmount - fees)
|
|
3786
|
-
BigInt(toChain.chainId),
|
|
3787
|
-
// destinationChainId
|
|
3788
|
-
ZERO_ADDRESS,
|
|
3789
|
-
// exclusiveRelayer (none)
|
|
3790
|
-
feeEstimate.timestamp,
|
|
3791
|
-
// quoteTimestamp
|
|
3792
|
-
fillDeadline,
|
|
3793
|
-
// fillDeadline
|
|
3794
|
-
0,
|
|
3795
|
-
// exclusivityDeadline (none)
|
|
3796
|
-
"0x"
|
|
3797
|
-
// message (empty)
|
|
3798
|
-
]
|
|
3799
|
-
});
|
|
3800
|
-
const receipt = await publicClient.waitForTransactionReceipt({ hash });
|
|
3801
|
-
const fee = request.amount - feeEstimate.outputAmount;
|
|
3802
|
-
console.log(`[bridge] Across deposit: ${hash} (${request.fromChain} \u2192 ${request.toChain})`);
|
|
3803
|
-
const result = {
|
|
3804
|
-
success: true,
|
|
3805
|
-
depositTxHash: hash,
|
|
3806
|
-
fromChain: request.fromChain,
|
|
3807
|
-
toChain: request.toChain,
|
|
3808
|
-
token: request.token,
|
|
3809
|
-
amount: request.amount,
|
|
3810
|
-
fee
|
|
3811
|
-
};
|
|
3812
|
-
const fill = await this.waitForFill(hash, request.fromChain);
|
|
3813
|
-
if (fill.filled) {
|
|
3814
|
-
result.fillTxHash = fill.fillTxHash;
|
|
3815
|
-
result.fillTimestamp = Date.now();
|
|
3816
|
-
console.log(`[bridge] Across fill confirmed: ${fill.fillTxHash}`);
|
|
3817
|
-
} else {
|
|
3818
|
-
console.warn(`[bridge] Across fill not confirmed within timeout \u2014 deposit was successful, fill may still arrive`);
|
|
3819
|
-
}
|
|
3820
|
-
return result;
|
|
3821
|
-
} catch (err) {
|
|
3822
|
-
return {
|
|
3823
|
-
success: false,
|
|
3824
|
-
depositTxHash: "0x0",
|
|
3825
|
-
fromChain: request.fromChain,
|
|
3826
|
-
toChain: request.toChain,
|
|
3827
|
-
token: request.token,
|
|
3828
|
-
amount: request.amount,
|
|
3829
|
-
fee: 0n,
|
|
3830
|
-
error: err.message
|
|
3831
|
-
};
|
|
3832
|
-
}
|
|
3833
|
-
}
|
|
3834
|
-
async waitForFill(depositTxHash, originChain) {
|
|
3835
|
-
const chainConfig = getChainConfig(originChain);
|
|
3836
|
-
if (!chainConfig) return { filled: false };
|
|
3837
|
-
const startTime = Date.now();
|
|
3838
|
-
const timeout = this.config.fillTimeoutMs;
|
|
3839
|
-
const interval = this.config.pollIntervalMs;
|
|
3840
|
-
while (Date.now() - startTime < timeout) {
|
|
3841
|
-
try {
|
|
3842
|
-
const params = new URLSearchParams({
|
|
3843
|
-
depositTxHash,
|
|
3844
|
-
originChainId: chainConfig.chainId.toString()
|
|
3845
|
-
});
|
|
3846
|
-
const res = await fetch(`${ACROSS_API_BASE}/deposit/status?${params}`);
|
|
3847
|
-
if (res.ok) {
|
|
3848
|
-
const data = await res.json();
|
|
3849
|
-
if (data.status === "filled" && data.fillTx) {
|
|
3850
|
-
return { filled: true, fillTxHash: data.fillTx };
|
|
3851
|
-
}
|
|
3852
|
-
if (data.status === "expired" || data.status === "refunded") {
|
|
3853
|
-
return { filled: false };
|
|
3854
|
-
}
|
|
3855
|
-
}
|
|
3856
|
-
} catch {
|
|
3857
|
-
}
|
|
3858
|
-
await new Promise((resolve2) => setTimeout(resolve2, interval));
|
|
3859
|
-
}
|
|
3860
|
-
return { filled: false };
|
|
3861
|
-
}
|
|
3862
|
-
/** Resolve the equivalent token address on the destination chain */
|
|
3863
|
-
resolveOutputToken(inputToken, fromChain, toChain) {
|
|
3864
|
-
const from = getChainConfig(fromChain);
|
|
3865
|
-
const to = getChainConfig(toChain);
|
|
3866
|
-
if (!from || !to) return inputToken;
|
|
3867
|
-
if (inputToken.toLowerCase() === from.usdcAddress.toLowerCase()) {
|
|
3868
|
-
return to.usdcAddress;
|
|
3869
|
-
}
|
|
3870
|
-
if (inputToken.toLowerCase() === from.wethAddress.toLowerCase()) {
|
|
3871
|
-
return to.wethAddress;
|
|
3872
|
-
}
|
|
3873
|
-
return inputToken;
|
|
3874
|
-
}
|
|
3875
|
-
};
|
|
3876
|
-
|
|
3877
|
-
// src/bridge/bridge-manager.ts
|
|
3878
|
-
var MIN_GAS_WEI = 100000000000000n;
|
|
3879
|
-
var BridgeManager = class {
|
|
3880
|
-
across;
|
|
3881
|
-
client;
|
|
3882
|
-
config;
|
|
3883
|
-
constructor(client, config) {
|
|
3884
|
-
this.config = config;
|
|
3885
|
-
this.client = client;
|
|
3886
|
-
this.across = new AcrossAdapter(client, config);
|
|
3887
|
-
}
|
|
3888
|
-
async estimateFee(request) {
|
|
3889
|
-
const bridge = request.bridge ?? this.config.defaultBridge;
|
|
3890
|
-
if (bridge !== "across") {
|
|
3891
|
-
throw new Error(`Unsupported bridge: ${bridge}. Only "across" is currently supported.`);
|
|
3892
|
-
}
|
|
3893
|
-
return this.across.estimateFee(request);
|
|
3894
|
-
}
|
|
3895
|
-
async bridge(request) {
|
|
3896
|
-
const bridge = request.bridge ?? this.config.defaultBridge;
|
|
3897
|
-
if (bridge !== "across") {
|
|
3898
|
-
return {
|
|
3899
|
-
success: false,
|
|
3900
|
-
depositTxHash: "0x0",
|
|
3901
|
-
fromChain: request.fromChain,
|
|
3902
|
-
toChain: request.toChain,
|
|
3903
|
-
token: request.token,
|
|
3904
|
-
amount: request.amount,
|
|
3905
|
-
fee: 0n,
|
|
3906
|
-
error: `Unsupported bridge: ${bridge}. Only "across" is currently supported.`
|
|
3907
|
-
};
|
|
3908
|
-
}
|
|
3909
|
-
try {
|
|
3910
|
-
const nativeBalance = await this.client.getNativeBalance(request.fromChain);
|
|
3911
|
-
if (nativeBalance < MIN_GAS_WEI) {
|
|
3912
|
-
const chainConfig = getChainConfig(request.fromChain);
|
|
3913
|
-
const nativeName = chainConfig?.nativeCurrency ?? "ETH";
|
|
3914
|
-
return {
|
|
3915
|
-
success: false,
|
|
3916
|
-
depositTxHash: "0x0",
|
|
3917
|
-
fromChain: request.fromChain,
|
|
3918
|
-
toChain: request.toChain,
|
|
3919
|
-
token: request.token,
|
|
3920
|
-
amount: request.amount,
|
|
3921
|
-
fee: 0n,
|
|
3922
|
-
error: `Insufficient ${nativeName} for gas on ${request.fromChain}. Balance: ${nativeBalance} wei. Need at least 0.0001 ${nativeName}.`
|
|
3923
|
-
};
|
|
3924
|
-
}
|
|
3925
|
-
} catch {
|
|
3926
|
-
}
|
|
3927
|
-
console.log(`[bridge] Bridging via Across: ${request.fromChain} \u2192 ${request.toChain} (${request.amount})`);
|
|
3928
|
-
return this.across.deposit(request);
|
|
3929
|
-
}
|
|
3930
|
-
};
|
|
3931
|
-
|
|
3932
|
-
// src/runtime.ts
|
|
3933
|
-
import { createWalletClient as createWalletClient2, http as http2, formatUnits } from "viem";
|
|
3934
|
-
import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
|
|
3935
|
-
import { arbitrum as arbitrum2 } from "viem/chains";
|
|
3936
|
-
var SDK_VERSION = "0.1.0";
|
|
3937
|
-
var AgentRuntime = class _AgentRuntime {
|
|
3938
|
-
config;
|
|
3939
|
-
relay;
|
|
3940
|
-
signal;
|
|
3941
|
-
store;
|
|
3942
|
-
positions;
|
|
3943
|
-
llm;
|
|
3944
|
-
strategy = null;
|
|
3945
|
-
risk;
|
|
3946
|
-
market;
|
|
3947
|
-
paper = null;
|
|
3948
|
-
// Venue clients
|
|
3949
|
-
hlClient = null;
|
|
3950
|
-
hlSigner = null;
|
|
3951
|
-
hlOrders = null;
|
|
3952
|
-
hlPositions = null;
|
|
3953
|
-
hlWs = null;
|
|
3954
|
-
pmClient = null;
|
|
3955
|
-
pmOrders = null;
|
|
3956
|
-
spotManager = null;
|
|
3957
|
-
bridgeManager = null;
|
|
3958
|
-
walletAddress = null;
|
|
3959
|
-
mode = "idle";
|
|
3960
|
-
cycleCount = 0;
|
|
3961
|
-
lastCycleAt = 0;
|
|
3962
|
-
tradingInterval = null;
|
|
3963
|
-
running = false;
|
|
3964
|
-
constructor(config) {
|
|
3965
|
-
this.config = config;
|
|
3966
|
-
this.store = new FileStore(`data/${config.agentId}-store.json`);
|
|
3967
|
-
this.positions = new PositionTracker(this.store);
|
|
3968
|
-
this.market = new MarketDataService(3e4, config.apiUrl);
|
|
3969
|
-
this.risk = new RiskManager(
|
|
3970
|
-
{
|
|
3971
|
-
maxPositionSizeBps: config.trading.maxPositionSizeBps,
|
|
3972
|
-
maxDailyLossBps: config.trading.maxDailyLossBps,
|
|
3973
|
-
maxConcurrentPositions: config.trading.maxConcurrentPositions,
|
|
3974
|
-
maxSlippageBps: config.trading.maxSlippageBps,
|
|
3975
|
-
minTradeValueUSD: config.trading.minTradeValueUSD
|
|
3976
|
-
},
|
|
3977
|
-
config.trading.initialCapitalUSD
|
|
3978
|
-
);
|
|
3979
|
-
this.llm = createLLMAdapter(config.llm);
|
|
3980
|
-
this.relay = new RelayClient({
|
|
3981
|
-
url: config.relay.url,
|
|
3982
|
-
agentId: config.agentId,
|
|
3983
|
-
token: config.apiToken,
|
|
3984
|
-
heartbeatIntervalMs: config.relay.heartbeatIntervalMs,
|
|
3985
|
-
reconnectMaxAttempts: config.relay.reconnectMaxAttempts,
|
|
3986
|
-
onCommand: (cmd) => this.handleCommand(cmd),
|
|
3987
|
-
onConnected: () => {
|
|
3988
|
-
console.log(`[runtime] Connected to command center`);
|
|
3989
|
-
this.sendStatus();
|
|
3990
|
-
},
|
|
3991
|
-
onDisconnected: () => {
|
|
3992
|
-
console.log(`[runtime] Disconnected from command center`);
|
|
3993
|
-
}
|
|
3994
|
-
});
|
|
3995
|
-
this.signal = new SignalReporter(this.relay);
|
|
3996
|
-
if (config.trading.mode === "paper") {
|
|
3997
|
-
this.paper = new PaperExecutor(config.trading.initialCapitalUSD ?? 1e4);
|
|
3998
|
-
}
|
|
3999
|
-
}
|
|
4000
|
-
async start() {
|
|
4001
|
-
console.log(`[runtime] Starting agent ${this.config.agentId}`);
|
|
4002
|
-
console.log(`[runtime] Mode: ${this.config.trading.mode}`);
|
|
4003
|
-
console.log(`[runtime] LLM: ${this.config.llm.provider}/${this.config.llm.model}`);
|
|
4004
|
-
try {
|
|
4005
|
-
this.strategy = await loadStrategy(this.config.strategy);
|
|
4006
|
-
console.log(`[runtime] Strategy loaded`);
|
|
4007
|
-
} catch (err) {
|
|
4008
|
-
console.error(`[runtime] Failed to load strategy:`, err.message);
|
|
4009
|
-
this.strategy = null;
|
|
4010
|
-
}
|
|
4011
|
-
if (this.config.trading.mode === "live") {
|
|
4012
|
-
await this.initializeVenues();
|
|
4013
|
-
}
|
|
4014
|
-
try {
|
|
4015
|
-
await this.relay.connect();
|
|
4016
|
-
this.signal.reportInfo("Agent Started", `Agent ${this.config.agentId} connected to command center`);
|
|
4017
|
-
} catch (err) {
|
|
4018
|
-
console.error(`[runtime] Failed to connect to relay:`, err.message);
|
|
4019
|
-
console.log(`[runtime] Will retry in background...`);
|
|
4020
|
-
}
|
|
4021
|
-
this.running = true;
|
|
4022
|
-
this.mode = "idle";
|
|
4023
|
-
this.sendStatus();
|
|
4024
|
-
if (this.config.trading.mode === "paper" || this.config.trading.mode === "live") {
|
|
4025
|
-
this.startTrading();
|
|
4026
|
-
}
|
|
4027
|
-
process.on("SIGTERM", () => this.stop());
|
|
4028
|
-
process.on("SIGINT", () => this.stop());
|
|
4029
|
-
}
|
|
4030
|
-
async stop() {
|
|
4031
|
-
console.log(`[runtime] Stopping agent ${this.config.agentId}`);
|
|
4032
|
-
this.running = false;
|
|
4033
|
-
this.stopTrading();
|
|
4034
|
-
if (this.hlWs) {
|
|
4035
|
-
this.hlWs.disconnect();
|
|
4036
|
-
}
|
|
4037
|
-
this.signal.reportInfo("Agent Stopped", `Agent ${this.config.agentId} shutting down`);
|
|
4038
|
-
this.relay.disconnect();
|
|
4039
|
-
console.log(`[runtime] Agent stopped`);
|
|
4040
|
-
process.exit(0);
|
|
4041
|
-
}
|
|
4042
|
-
// ── VENUE INITIALIZATION ───────────────────────────────────
|
|
4043
|
-
async initializeVenues() {
|
|
4044
|
-
const privateKey = this.config.wallet?.privateKey;
|
|
4045
|
-
if (!privateKey) {
|
|
4046
|
-
console.log("[runtime] No wallet configured \u2014 live trading disabled");
|
|
4047
|
-
return;
|
|
4048
|
-
}
|
|
4049
|
-
const walletAccount = privateKeyToAccount2(privateKey);
|
|
4050
|
-
this.walletAddress = walletAccount.address;
|
|
4051
|
-
const hlConfig = this.config.venues?.hyperliquid_perp;
|
|
4052
|
-
if (hlConfig?.enabled) {
|
|
4053
|
-
try {
|
|
4054
|
-
const account = walletAccount;
|
|
4055
|
-
const walletClient = createWalletClient2({
|
|
4056
|
-
account,
|
|
4057
|
-
chain: arbitrum2,
|
|
4058
|
-
transport: http2()
|
|
4059
|
-
});
|
|
4060
|
-
const perpConfig = {
|
|
4061
|
-
enabled: true,
|
|
4062
|
-
apiUrl: hlConfig.apiUrl,
|
|
4063
|
-
wsUrl: hlConfig.wsUrl,
|
|
4064
|
-
maxLeverage: hlConfig.maxLeverage,
|
|
4065
|
-
maxNotionalUSD: hlConfig.maxNotionalUSD,
|
|
4066
|
-
allowedInstruments: hlConfig.allowedInstruments
|
|
4067
|
-
};
|
|
4068
|
-
this.hlClient = new HyperliquidClient(perpConfig);
|
|
4069
|
-
this.hlSigner = new HyperliquidSigner(walletClient);
|
|
4070
|
-
this.hlOrders = new HyperliquidOrderManager(this.hlClient, this.hlSigner, perpConfig);
|
|
4071
|
-
this.hlPositions = new HyperliquidPositionManager(this.hlClient, account.address);
|
|
4072
|
-
this.hlWs = new HyperliquidWebSocket(perpConfig, account.address, this.hlClient);
|
|
4073
|
-
await this.hlClient.getMeta();
|
|
4074
|
-
this.hlWs.onFillReceived((fill) => {
|
|
4075
|
-
this.handleHyperliquidFill(fill);
|
|
4076
|
-
});
|
|
4077
|
-
this.hlWs.onLiquidationDetected((instrument, size) => {
|
|
4078
|
-
this.signal.reportError("Liquidation Detected", `${instrument}: ${size} liquidated`);
|
|
4079
|
-
});
|
|
4080
|
-
await this.hlWs.connect();
|
|
4081
|
-
console.log(`[runtime] Hyperliquid initialized (${account.address})`);
|
|
4082
|
-
} catch (err) {
|
|
4083
|
-
console.error("[runtime] Failed to initialize Hyperliquid:", err.message);
|
|
4084
|
-
}
|
|
4085
|
-
}
|
|
4086
|
-
const pmConfig = this.config.venues?.polymarket;
|
|
4087
|
-
if (pmConfig?.enabled) {
|
|
4088
|
-
try {
|
|
4089
|
-
const predConfig = {
|
|
4090
|
-
enabled: true,
|
|
4091
|
-
clobApiUrl: pmConfig.clobApiUrl,
|
|
4092
|
-
gammaApiUrl: pmConfig.gammaApiUrl,
|
|
4093
|
-
maxNotionalUSD: pmConfig.maxNotionalUSD,
|
|
4094
|
-
maxTotalExposureUSD: pmConfig.maxTotalExposureUSD,
|
|
4095
|
-
allowedCategories: pmConfig.allowedCategories
|
|
4096
|
-
};
|
|
4097
|
-
this.pmClient = new PolymarketClient(privateKey, predConfig);
|
|
4098
|
-
await this.pmClient.initialize();
|
|
4099
|
-
this.pmOrders = new PolymarketOrderManager(this.pmClient, predConfig);
|
|
4100
|
-
console.log(`[runtime] Polymarket initialized`);
|
|
4101
|
-
} catch (err) {
|
|
4102
|
-
console.error("[runtime] Failed to initialize Polymarket:", err.message);
|
|
4103
|
-
}
|
|
4104
|
-
}
|
|
4105
|
-
const spotConfig = this.config.venues?.spot;
|
|
4106
|
-
if (spotConfig?.enabled) {
|
|
4107
|
-
try {
|
|
4108
|
-
const cfg = {
|
|
4109
|
-
enabled: true,
|
|
4110
|
-
chains: spotConfig.chains,
|
|
4111
|
-
defaultChain: spotConfig.defaultChain,
|
|
4112
|
-
maxSlippageBps: spotConfig.maxSlippageBps,
|
|
4113
|
-
maxSwapValueUSD: spotConfig.maxSwapValueUSD
|
|
4114
|
-
};
|
|
4115
|
-
this.spotManager = new SpotSwapManager(privateKey, cfg);
|
|
4116
|
-
console.log(`[runtime] Spot DEX initialized (chains: ${spotConfig.chains.join(", ")})`);
|
|
4117
|
-
} catch (err) {
|
|
4118
|
-
console.error("[runtime] Failed to initialize Spot DEX:", err.message);
|
|
4119
|
-
}
|
|
4120
|
-
}
|
|
4121
|
-
const bridgeConfig = this.config.venues?.bridge;
|
|
4122
|
-
if (bridgeConfig?.enabled) {
|
|
4123
|
-
try {
|
|
4124
|
-
if (!this.spotManager) {
|
|
4125
|
-
const minimalSpotConfig = {
|
|
4126
|
-
enabled: true,
|
|
4127
|
-
chains: ["base", "ethereum", "arbitrum", "polygon"],
|
|
4128
|
-
defaultChain: "base",
|
|
4129
|
-
maxSlippageBps: 50,
|
|
4130
|
-
maxSwapValueUSD: 1e4
|
|
4131
|
-
};
|
|
4132
|
-
this.spotManager = new SpotSwapManager(privateKey, minimalSpotConfig);
|
|
4133
|
-
}
|
|
4134
|
-
const cfg = {
|
|
4135
|
-
enabled: true,
|
|
4136
|
-
defaultBridge: bridgeConfig.defaultBridge,
|
|
4137
|
-
maxBridgeValueUSD: bridgeConfig.maxBridgeValueUSD,
|
|
4138
|
-
fillTimeoutMs: bridgeConfig.fillTimeoutMs,
|
|
4139
|
-
pollIntervalMs: bridgeConfig.pollIntervalMs
|
|
4140
|
-
};
|
|
4141
|
-
this.bridgeManager = new BridgeManager(this.spotManager.client, cfg);
|
|
4142
|
-
console.log(`[runtime] Bridge initialized`);
|
|
4143
|
-
} catch (err) {
|
|
4144
|
-
console.error("[runtime] Failed to initialize Bridge:", err.message);
|
|
4145
|
-
}
|
|
4146
|
-
}
|
|
4147
|
-
}
|
|
4148
|
-
// ── TRADING LOOP ───────────────────────────────────────────
|
|
4149
|
-
startTrading() {
|
|
4150
|
-
if (this.tradingInterval) return;
|
|
4151
|
-
this.mode = this.config.trading.mode === "paper" ? "paper" : "trading";
|
|
4152
|
-
console.log(`[runtime] Trading started (${this.mode}, interval: ${this.config.trading.tradingIntervalMs}ms)`);
|
|
4153
|
-
this.sendStatus();
|
|
4154
|
-
this.signal.reportInfo("Trading Started", `Mode: ${this.mode}`);
|
|
4155
|
-
this.runCycle();
|
|
4156
|
-
this.tradingInterval = setInterval(() => {
|
|
4157
|
-
this.runCycle();
|
|
4158
|
-
}, this.config.trading.tradingIntervalMs);
|
|
4159
|
-
}
|
|
4160
|
-
stopTrading() {
|
|
4161
|
-
if (this.tradingInterval) {
|
|
4162
|
-
clearInterval(this.tradingInterval);
|
|
4163
|
-
this.tradingInterval = null;
|
|
4164
|
-
}
|
|
4165
|
-
this.mode = "idle";
|
|
4166
|
-
this.sendStatus();
|
|
4167
|
-
console.log(`[runtime] Trading stopped`);
|
|
4168
|
-
}
|
|
4169
|
-
async runCycle() {
|
|
4170
|
-
if (!this.running || !this.strategy) return;
|
|
4171
|
-
this.cycleCount++;
|
|
4172
|
-
this.lastCycleAt = Date.now();
|
|
4173
|
-
try {
|
|
4174
|
-
const prices = this.market.getPrices();
|
|
4175
|
-
const positionSummary = this.positions.getSummary(prices);
|
|
4176
|
-
const signals = await this.strategy({
|
|
4177
|
-
llm: this.llm,
|
|
4178
|
-
market: this.market,
|
|
4179
|
-
position: positionSummary,
|
|
4180
|
-
store: this.store,
|
|
4181
|
-
config: this.config.trading,
|
|
4182
|
-
log: (msg) => {
|
|
4183
|
-
console.log(`[strategy] ${msg}`);
|
|
4184
|
-
this.signal.reportInfo("Strategy Log", msg);
|
|
4185
|
-
}
|
|
4186
|
-
});
|
|
4187
|
-
if (!Array.isArray(signals) || signals.length === 0) {
|
|
4188
|
-
await this.pollPredictionFills();
|
|
4189
|
-
this.sendStatus();
|
|
4190
|
-
return;
|
|
4191
|
-
}
|
|
4192
|
-
const filtered = this.risk.filterSignals(signals, this.market, positionSummary.openPositions.length);
|
|
4193
|
-
for (const sig of filtered) {
|
|
4194
|
-
try {
|
|
4195
|
-
await this.executeSignal(sig);
|
|
4196
|
-
} catch (err) {
|
|
4197
|
-
console.error(`[runtime] Trade execution error:`, err.message);
|
|
4198
|
-
this.signal.reportError("Trade Error", err.message);
|
|
4199
|
-
}
|
|
4200
|
-
}
|
|
4201
|
-
await this.pollPredictionFills();
|
|
4202
|
-
} catch (err) {
|
|
4203
|
-
console.error(`[runtime] Cycle error:`, err.message);
|
|
4204
|
-
this.signal.reportError("Cycle Error", err.message);
|
|
4205
|
-
this.mode = "idle";
|
|
4206
|
-
}
|
|
4207
|
-
this.sendStatus();
|
|
4208
|
-
}
|
|
4209
|
-
// ── SIGNAL EXECUTION ───────────────────────────────────────
|
|
4210
|
-
async executeSignal(sig) {
|
|
4211
|
-
const venue = sig.venue;
|
|
4212
|
-
if (this.paper) {
|
|
4213
|
-
const trade = this.paper.execute(sig, this.market);
|
|
4214
|
-
if (trade) {
|
|
4215
|
-
this.signal.reportTrade({
|
|
4216
|
-
...sig,
|
|
4217
|
-
price: trade.entryPrice,
|
|
4218
|
-
fee: trade.fee,
|
|
4219
|
-
venue: trade.venue,
|
|
4220
|
-
venueFillId: trade.id,
|
|
4221
|
-
venueTimestamp: new Date(trade.timestamp).toISOString()
|
|
4222
|
-
});
|
|
4223
|
-
}
|
|
4224
|
-
return;
|
|
4225
|
-
}
|
|
4226
|
-
if (venue === "hyperliquid_perp" && this.hlOrders) {
|
|
4227
|
-
await this.executeHyperliquidSignal(sig);
|
|
4228
|
-
} else if (venue === "polymarket" && this.pmOrders) {
|
|
4229
|
-
await this.executePolymarketSignal(sig);
|
|
4230
|
-
} else if ((venue === "uniswap" || venue === "aerodrome" || venue === "sushiswap") && this.spotManager) {
|
|
4231
|
-
await this.executeSpotSignal(sig);
|
|
4232
|
-
} else if (venue === "across" && this.bridgeManager) {
|
|
4233
|
-
await this.executeBridgeSignal(sig);
|
|
4234
|
-
} else if (venue === "hyperliquid_deposit" && this.hlSigner) {
|
|
4235
|
-
await this.executeHyperliquidDeposit(sig);
|
|
4236
|
-
} else if (venue === "hyperliquid_withdraw" && this.hlSigner) {
|
|
4237
|
-
await this.executeHyperliquidWithdraw(sig);
|
|
4238
|
-
} else {
|
|
4239
|
-
console.log(`[runtime] No executor for venue "${venue}" \u2014 signal dropped`);
|
|
4240
|
-
}
|
|
4241
|
-
}
|
|
4242
|
-
async executeHyperliquidSignal(sig) {
|
|
4243
|
-
if (!this.hlOrders) return;
|
|
4244
|
-
const action = sig.side === "long" ? "open_long" : sig.side === "short" ? "open_short" : sig.side === "buy" ? "open_long" : "close_long";
|
|
4245
|
-
const result = await this.hlOrders.placeOrder({
|
|
4246
|
-
action,
|
|
4247
|
-
instrument: sig.symbol,
|
|
4248
|
-
size: sig.size,
|
|
4249
|
-
price: sig.price,
|
|
4250
|
-
leverage: sig.leverage ?? 1,
|
|
4251
|
-
orderType: sig.orderType ?? "market",
|
|
4252
|
-
reduceOnly: action.startsWith("close"),
|
|
4253
|
-
confidence: sig.confidence ?? 1,
|
|
4254
|
-
reasoning: sig.reasoning
|
|
4255
|
-
});
|
|
4256
|
-
if (result.success && result.status === "filled") {
|
|
4257
|
-
console.log(`[runtime] HL order filled: ${sig.symbol} ${result.avgPrice}`);
|
|
4258
|
-
} else if (result.success && result.status === "resting") {
|
|
4259
|
-
console.log(`[runtime] HL order resting: ${sig.symbol} (oid: ${result.orderId})`);
|
|
4260
|
-
} else {
|
|
4261
|
-
console.warn(`[runtime] HL order failed: ${result.error}`);
|
|
4262
|
-
this.signal.reportError("Perp Order Failed", `${sig.symbol}: ${result.error}`);
|
|
4263
|
-
}
|
|
4264
|
-
}
|
|
4265
|
-
async executePolymarketSignal(sig) {
|
|
4266
|
-
if (!this.pmOrders) return;
|
|
4267
|
-
const action = sig.side === "buy" ? "buy_yes" : sig.side === "sell" ? "sell_yes" : sig.side === "long" ? "buy_yes" : "sell_yes";
|
|
4268
|
-
const result = await this.pmOrders.executeSignal({
|
|
4269
|
-
action,
|
|
4270
|
-
marketConditionId: sig.symbol,
|
|
4271
|
-
marketQuestion: sig.reasoning ?? sig.symbol,
|
|
4272
|
-
outcomeIndex: 0,
|
|
4273
|
-
amount: sig.size,
|
|
4274
|
-
limitPrice: sig.price,
|
|
4275
|
-
orderType: sig.orderType ?? "limit",
|
|
4276
|
-
confidence: sig.confidence ?? 1,
|
|
4277
|
-
reasoning: sig.reasoning
|
|
4278
|
-
});
|
|
4279
|
-
if (result.success) {
|
|
4280
|
-
console.log(`[runtime] PM order placed: ${sig.symbol}`);
|
|
4281
|
-
} else {
|
|
4282
|
-
console.warn(`[runtime] PM order failed: ${result.error}`);
|
|
4283
|
-
this.signal.reportError("Prediction Order Failed", `${sig.symbol}: ${result.error}`);
|
|
4284
|
-
}
|
|
4285
|
-
}
|
|
4286
|
-
async executeSpotSignal(sig) {
|
|
4287
|
-
if (!this.spotManager) return;
|
|
4288
|
-
const chain = sig.chain ?? this.config.venues?.spot?.defaultChain ?? "base";
|
|
4289
|
-
const dex = sig.venue === "aerodrome" ? "aerodrome" : "uniswap";
|
|
4290
|
-
const parts = sig.symbol.split("/");
|
|
4291
|
-
const isBuy = sig.side === "buy" || sig.side === "long";
|
|
4292
|
-
const tokenIn = isBuy ? parts[1] ?? "USDC" : parts[0] ?? sig.symbol;
|
|
4293
|
-
const tokenOut = isBuy ? parts[0] ?? sig.symbol : parts[1] ?? "USDC";
|
|
4294
|
-
const tokenInAddress = this.spotManager.resolveToken(tokenIn, chain);
|
|
4295
|
-
const decimals = await this.spotManager.client.getDecimals(tokenInAddress, chain);
|
|
4296
|
-
const amountIn = BigInt(Math.floor(sig.size * 10 ** decimals));
|
|
4297
|
-
const result = await this.spotManager.executeSwap({
|
|
4298
|
-
tokenIn,
|
|
4299
|
-
tokenOut,
|
|
4300
|
-
amountIn,
|
|
4301
|
-
chain,
|
|
4302
|
-
dex
|
|
4303
|
-
});
|
|
4304
|
-
if (result.success) {
|
|
4305
|
-
const tokenOutSymbol = await this.spotManager.client.getSymbol(result.tokenOut, chain);
|
|
4306
|
-
const tokenInSymbol = await this.spotManager.client.getSymbol(result.tokenIn, chain);
|
|
4307
|
-
const tokenOutDecimals = await this.spotManager.client.getDecimals(result.tokenOut, chain);
|
|
4308
|
-
const amountOutFloat = Number(result.amountOut) / 10 ** tokenOutDecimals;
|
|
4309
|
-
const amountInFloat = Number(result.amountIn) / 10 ** decimals;
|
|
4310
|
-
const tradeSignal = {
|
|
4311
|
-
venue: result.dex,
|
|
4312
|
-
chain,
|
|
4313
|
-
symbol: `${parts[0] ?? sig.symbol}`,
|
|
4314
|
-
side: sig.side,
|
|
4315
|
-
size: isBuy ? amountOutFloat : amountInFloat,
|
|
4316
|
-
price: result.effectivePrice,
|
|
4317
|
-
fee: Number(result.gasCost) / 1e18,
|
|
4318
|
-
venueFillId: result.txHash,
|
|
4319
|
-
venueTimestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4320
|
-
};
|
|
4321
|
-
const baseToken = parts[0] ?? sig.symbol;
|
|
4322
|
-
if (isBuy) {
|
|
4323
|
-
this.positions.recordBuy(
|
|
4324
|
-
baseToken,
|
|
4325
|
-
amountOutFloat,
|
|
4326
|
-
result.effectivePrice,
|
|
4327
|
-
tradeSignal.fee,
|
|
4328
|
-
result.dex,
|
|
4329
|
-
chain,
|
|
4330
|
-
result.txHash
|
|
4331
|
-
);
|
|
4332
|
-
} else {
|
|
4333
|
-
this.positions.recordSell(
|
|
4334
|
-
baseToken,
|
|
4335
|
-
amountInFloat,
|
|
4336
|
-
result.effectivePrice,
|
|
4337
|
-
tradeSignal.fee,
|
|
4338
|
-
result.dex,
|
|
4339
|
-
chain,
|
|
4340
|
-
result.txHash
|
|
4341
|
-
);
|
|
4342
|
-
}
|
|
4343
|
-
this.signal.reportSpotFill(tradeSignal);
|
|
4344
|
-
console.log(`[runtime] Spot swap filled: ${tokenInSymbol} \u2192 ${tokenOutSymbol} on ${result.dex}/${chain}`);
|
|
4345
|
-
} else {
|
|
4346
|
-
console.warn(`[runtime] Spot swap failed: ${result.error}`);
|
|
4347
|
-
this.signal.reportError("Spot Swap Failed", `${sig.symbol}: ${result.error}`);
|
|
4348
|
-
}
|
|
4349
|
-
}
|
|
4350
|
-
async executeBridgeSignal(sig) {
|
|
4351
|
-
if (!this.bridgeManager || !this.spotManager) return;
|
|
4352
|
-
const toChain = sig.chain ?? "arbitrum";
|
|
4353
|
-
const fromChain = sig.orderType ?? "base";
|
|
4354
|
-
const tokenAddress = this.spotManager.resolveToken(sig.symbol, fromChain);
|
|
4355
|
-
const decimals = await this.spotManager.client.getDecimals(tokenAddress, fromChain);
|
|
4356
|
-
const amount = BigInt(Math.floor(sig.size * 10 ** decimals));
|
|
4357
|
-
const result = await this.bridgeManager.bridge({
|
|
4358
|
-
token: tokenAddress,
|
|
4359
|
-
amount,
|
|
4360
|
-
fromChain,
|
|
4361
|
-
toChain
|
|
4362
|
-
});
|
|
4363
|
-
if (result.success) {
|
|
4364
|
-
const tradeSignal = {
|
|
4365
|
-
venue: "across",
|
|
4366
|
-
chain: toChain,
|
|
4367
|
-
symbol: sig.symbol,
|
|
4368
|
-
side: "buy",
|
|
4369
|
-
size: sig.size,
|
|
4370
|
-
price: sig.price || 1,
|
|
4371
|
-
fee: Number(result.fee) / 10 ** decimals,
|
|
4372
|
-
venueFillId: result.depositTxHash,
|
|
4373
|
-
venueTimestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4374
|
-
};
|
|
4375
|
-
this.signal.reportBridgeFill(tradeSignal);
|
|
4376
|
-
console.log(`[runtime] Bridge completed: ${fromChain} \u2192 ${toChain} via Across`);
|
|
4377
|
-
} else {
|
|
4378
|
-
console.warn(`[runtime] Bridge failed: ${result.error}`);
|
|
4379
|
-
this.signal.reportError("Bridge Failed", `${sig.symbol} ${fromChain}\u2192${toChain}: ${result.error}`);
|
|
4380
|
-
}
|
|
4381
|
-
}
|
|
4382
|
-
// ── HYPERLIQUID DEPOSIT / WITHDRAW ────────────────────────
|
|
4383
|
-
/** Hyperliquid bridge contract on Arbitrum — accepts plain USDC transfers */
|
|
4384
|
-
static HL_BRIDGE_CONTRACT = "0x2Df1c51E09aECF9cacB7bc98cB1742757f163dF7";
|
|
4385
|
-
/**
|
|
4386
|
-
* Deposit USDC into Hyperliquid by transferring to the bridge contract on Arbitrum.
|
|
4387
|
-
* Signal format: venue='hyperliquid_deposit', size=amount in USDC, chain is ignored (always Arbitrum).
|
|
4388
|
-
* The wallet must already have USDC + ETH for gas on Arbitrum.
|
|
4389
|
-
*/
|
|
4390
|
-
async executeHyperliquidDeposit(sig) {
|
|
4391
|
-
if (!this.spotManager) {
|
|
4392
|
-
this.signal.reportError("HL Deposit Failed", "Spot manager not initialized (needed for chain clients)");
|
|
4393
|
-
return;
|
|
4394
|
-
}
|
|
4395
|
-
const arbConfig = getChainConfig("arbitrum");
|
|
4396
|
-
if (!arbConfig) {
|
|
4397
|
-
this.signal.reportError("HL Deposit Failed", "Arbitrum chain config not found");
|
|
4398
|
-
return;
|
|
4399
|
-
}
|
|
4400
|
-
const usdcAddress = arbConfig.usdcAddress;
|
|
4401
|
-
const amount = BigInt(Math.floor(sig.size * 1e6));
|
|
4402
|
-
const MIN_GAS_WEI2 = 100000000000000n;
|
|
4403
|
-
try {
|
|
4404
|
-
const gasBalance = await this.spotManager.client.getNativeBalance("arbitrum");
|
|
4405
|
-
if (gasBalance < MIN_GAS_WEI2) {
|
|
4406
|
-
this.signal.reportError("HL Deposit Failed", `Insufficient ETH for gas on Arbitrum. Balance: ${formatUnits(gasBalance, 18)} ETH. Need at least 0.0001 ETH.`);
|
|
4407
|
-
return;
|
|
4408
|
-
}
|
|
4409
|
-
} catch {
|
|
4410
|
-
}
|
|
4411
|
-
try {
|
|
4412
|
-
const usdcBalance = await this.spotManager.client.getBalance(usdcAddress, "arbitrum");
|
|
4413
|
-
if (usdcBalance < amount) {
|
|
4414
|
-
this.signal.reportError("HL Deposit Failed", `Insufficient USDC on Arbitrum. Have ${formatUnits(usdcBalance, 6)}, need ${formatUnits(amount, 6)}. Bridge USDC to Arbitrum first.`);
|
|
4415
|
-
return;
|
|
4416
|
-
}
|
|
4417
|
-
} catch (err) {
|
|
4418
|
-
this.signal.reportError("HL Deposit Failed", `Could not check USDC balance: ${err.message}`);
|
|
4419
|
-
return;
|
|
4420
|
-
}
|
|
4421
|
-
try {
|
|
4422
|
-
const { publicClient, walletClient } = this.spotManager.client.getClients("arbitrum");
|
|
4423
|
-
const hash = await walletClient.writeContract({
|
|
4424
|
-
address: usdcAddress,
|
|
4425
|
-
abi: ERC20_ABI,
|
|
4426
|
-
functionName: "transfer",
|
|
4427
|
-
args: [_AgentRuntime.HL_BRIDGE_CONTRACT, amount]
|
|
4428
|
-
});
|
|
4429
|
-
const receipt = await publicClient.waitForTransactionReceipt({ hash });
|
|
4430
|
-
if (receipt.status === "reverted") {
|
|
4431
|
-
this.signal.reportError("HL Deposit Failed", `Transfer reverted: ${hash}`);
|
|
4432
|
-
return;
|
|
4433
|
-
}
|
|
4434
|
-
console.log(`[runtime] HL deposit: ${formatUnits(amount, 6)} USDC \u2192 Hyperliquid (tx: ${hash})`);
|
|
4435
|
-
this.signal.reportInfo("HL Deposit", `Deposited ${formatUnits(amount, 6)} USDC to Hyperliquid`);
|
|
4436
|
-
} catch (err) {
|
|
4437
|
-
console.error("[runtime] HL deposit failed:", err.message);
|
|
4438
|
-
this.signal.reportError("HL Deposit Failed", err.message);
|
|
4439
|
-
}
|
|
4440
|
-
}
|
|
4441
|
-
/**
|
|
4442
|
-
* Withdraw USDC from Hyperliquid to the wallet on Arbitrum.
|
|
4443
|
-
* Signal format: venue='hyperliquid_withdraw', size=amount in USDC.
|
|
4444
|
-
* Uses the withdraw3 EIP-712 signing scheme.
|
|
4445
|
-
*/
|
|
4446
|
-
async executeHyperliquidWithdraw(sig) {
|
|
4447
|
-
if (!this.hlSigner || !this.hlClient) {
|
|
4448
|
-
this.signal.reportError("HL Withdraw Failed", "Hyperliquid signer/client not initialized");
|
|
4449
|
-
return;
|
|
4450
|
-
}
|
|
4451
|
-
const destination = this.hlSigner.getAddress();
|
|
4452
|
-
const amount = sig.size.toString();
|
|
4453
|
-
try {
|
|
4454
|
-
const { action, signature, nonce } = await this.hlSigner.signWithdraw(destination, amount);
|
|
4455
|
-
const apiUrl = this.config.venues?.hyperliquid_perp?.apiUrl ?? "https://api.hyperliquid.xyz";
|
|
4456
|
-
const resp = await fetch(`${apiUrl}/exchange`, {
|
|
4457
|
-
method: "POST",
|
|
4458
|
-
headers: { "Content-Type": "application/json" },
|
|
4459
|
-
body: JSON.stringify({ action, nonce, signature, vaultAddress: null })
|
|
4460
|
-
});
|
|
4461
|
-
if (!resp.ok) {
|
|
4462
|
-
const text = await resp.text();
|
|
4463
|
-
throw new Error(`HL Exchange API error: ${resp.status} ${text}`);
|
|
4464
|
-
}
|
|
4465
|
-
const data = await resp.json();
|
|
4466
|
-
if (data.status !== "ok") {
|
|
4467
|
-
throw new Error(`HL withdraw3 failed: ${JSON.stringify(data)}`);
|
|
4468
|
-
}
|
|
4469
|
-
console.log(`[runtime] HL withdrawal: ${amount} USDC \u2192 ${destination} on Arbitrum`);
|
|
4470
|
-
this.signal.reportInfo("HL Withdrawal", `Withdrew ${amount} USDC from Hyperliquid to ${destination}`);
|
|
4471
|
-
} catch (err) {
|
|
4472
|
-
console.error("[runtime] HL withdrawal failed:", err.message);
|
|
4473
|
-
this.signal.reportError("HL Withdraw Failed", err.message);
|
|
4474
|
-
}
|
|
4475
|
-
}
|
|
4476
|
-
// ── FILL HANDLING ──────────────────────────────────────────
|
|
4477
|
-
handleHyperliquidFill(fill) {
|
|
4478
|
-
const tradeSignal = {
|
|
4479
|
-
venue: "hyperliquid_perp",
|
|
4480
|
-
symbol: fill.coin,
|
|
4481
|
-
side: fill.side === "B" ? "long" : "short",
|
|
4482
|
-
size: parseFloat(fill.sz),
|
|
4483
|
-
price: parseFloat(fill.px),
|
|
4484
|
-
fee: parseFloat(fill.fee),
|
|
4485
|
-
venueFillId: fill.hash,
|
|
4486
|
-
venueTimestamp: new Date(fill.time).toISOString(),
|
|
4487
|
-
orderType: fill.isMaker ? "limit" : "market"
|
|
4488
|
-
};
|
|
4489
|
-
const action = fill.side === "B" ? "buy" : "sell";
|
|
4490
|
-
if (action === "buy") {
|
|
4491
|
-
this.positions.recordBuy(
|
|
4492
|
-
fill.coin,
|
|
4493
|
-
parseFloat(fill.sz),
|
|
4494
|
-
parseFloat(fill.px),
|
|
4495
|
-
parseFloat(fill.fee),
|
|
4496
|
-
"hyperliquid_perp",
|
|
4497
|
-
void 0,
|
|
4498
|
-
fill.hash
|
|
4499
|
-
);
|
|
4500
|
-
} else {
|
|
4501
|
-
this.positions.recordSell(
|
|
4502
|
-
fill.coin,
|
|
4503
|
-
parseFloat(fill.sz),
|
|
4504
|
-
parseFloat(fill.px),
|
|
4505
|
-
parseFloat(fill.fee),
|
|
4506
|
-
"hyperliquid_perp",
|
|
4507
|
-
void 0,
|
|
4508
|
-
fill.hash
|
|
4509
|
-
);
|
|
4510
|
-
}
|
|
4511
|
-
this.signal.reportPerpFill(tradeSignal);
|
|
4512
|
-
}
|
|
4513
|
-
async pollPredictionFills() {
|
|
4514
|
-
if (!this.pmOrders) return;
|
|
4515
|
-
try {
|
|
4516
|
-
const newFills = await this.pmOrders.pollNewFills();
|
|
4517
|
-
for (const fill of newFills) {
|
|
4518
|
-
const tradeSignal = {
|
|
4519
|
-
venue: "polymarket",
|
|
4520
|
-
chain: "polygon",
|
|
4521
|
-
symbol: encodePredictionInstrument(fill.marketConditionId, fill.outcomeIndex),
|
|
4522
|
-
side: fill.side === "BUY" ? "buy" : "sell",
|
|
4523
|
-
size: parseFloat(fill.size),
|
|
4524
|
-
price: parseFloat(fill.price),
|
|
4525
|
-
fee: parseFloat(fill.fee),
|
|
4526
|
-
venueFillId: fill.tradeId,
|
|
4527
|
-
venueTimestamp: new Date(fill.timestamp).toISOString()
|
|
4528
|
-
};
|
|
4529
|
-
this.signal.reportPredictionFill(tradeSignal);
|
|
4530
|
-
}
|
|
4531
|
-
} catch (err) {
|
|
4532
|
-
console.warn("[runtime] Prediction fill poll error:", err.message);
|
|
4533
|
-
}
|
|
4534
|
-
}
|
|
4535
|
-
// ── COMMANDS ───────────────────────────────────────────────
|
|
4536
|
-
handleCommand(command) {
|
|
4537
|
-
console.log(`[runtime] Command received: ${command.type} (${command.id})`);
|
|
4538
|
-
switch (command.type) {
|
|
4539
|
-
case "start_trading":
|
|
4540
|
-
this.startTrading();
|
|
4541
|
-
this.relay.sendCommandAck(command.id, true, "Trading started");
|
|
4542
|
-
break;
|
|
4543
|
-
case "stop_trading":
|
|
4544
|
-
this.stopTrading();
|
|
4545
|
-
this.relay.sendCommandAck(command.id, true, "Trading stopped");
|
|
4546
|
-
break;
|
|
4547
|
-
case "pause_trading":
|
|
4548
|
-
this.stopTrading();
|
|
4549
|
-
this.relay.sendCommandAck(command.id, true, "Trading paused");
|
|
4550
|
-
break;
|
|
4551
|
-
case "update_risk_params":
|
|
4552
|
-
if (command.params) {
|
|
4553
|
-
this.risk.updateParams(command.params);
|
|
4554
|
-
this.sendStatus();
|
|
4555
|
-
this.relay.sendCommandAck(command.id, true, "Risk params updated");
|
|
4556
|
-
} else {
|
|
4557
|
-
this.relay.sendCommandAck(command.id, false, "No params provided");
|
|
4558
|
-
}
|
|
4559
|
-
break;
|
|
4560
|
-
case "get_status":
|
|
4561
|
-
this.sendStatus();
|
|
4562
|
-
this.relay.sendCommandAck(command.id, true);
|
|
4563
|
-
break;
|
|
4564
|
-
case "reload_config":
|
|
4565
|
-
this.reloadConfig(command);
|
|
4566
|
-
break;
|
|
4567
|
-
default:
|
|
4568
|
-
console.warn(`[runtime] Unknown command: ${command.type}`);
|
|
4569
|
-
this.relay.sendCommandAck(command.id, false, `Unknown command: ${command.type}`);
|
|
4570
|
-
}
|
|
4571
|
-
}
|
|
4572
|
-
async reloadConfig(command) {
|
|
4573
|
-
try {
|
|
4574
|
-
const apiBase = this.config.apiUrl || this.config.relay.url.replace(/\/ws\/.*/, "");
|
|
4575
|
-
const res = await fetch(`${apiBase}/v1/agents/${this.config.agentId}`, {
|
|
4576
|
-
headers: { "Authorization": `Bearer ${this.config.apiToken}` }
|
|
4577
|
-
});
|
|
4578
|
-
if (!res.ok) {
|
|
4579
|
-
throw new Error(`API returned ${res.status}`);
|
|
4580
|
-
}
|
|
4581
|
-
const agent = await res.json();
|
|
4582
|
-
if (agent.config) {
|
|
4583
|
-
const cfg = agent.config;
|
|
4584
|
-
const risk = cfg.risk;
|
|
4585
|
-
if (risk) {
|
|
4586
|
-
const updates = {};
|
|
4587
|
-
if (typeof risk.maxPositionSize === "number") updates.maxPositionSizeBps = risk.maxPositionSize * 100;
|
|
4588
|
-
if (typeof risk.maxDailyLoss === "number") updates.maxDailyLossBps = risk.maxDailyLoss * 100;
|
|
4589
|
-
if (typeof risk.maxConcurrentPositions === "number") updates.maxConcurrentPositions = risk.maxConcurrentPositions;
|
|
4590
|
-
if (typeof risk.slippageTolerance === "number") updates.maxSlippageBps = risk.slippageTolerance * 100;
|
|
4591
|
-
if (typeof risk.tradingInterval === "number") {
|
|
4592
|
-
this.config.trading.tradingIntervalMs = risk.tradingInterval * 1e3;
|
|
4593
|
-
}
|
|
4594
|
-
if (Object.keys(updates).length > 0) {
|
|
4595
|
-
this.risk.updateParams(updates);
|
|
4596
|
-
}
|
|
4597
|
-
}
|
|
4598
|
-
console.log("[runtime] Config reloaded from API");
|
|
4599
|
-
this.sendStatus();
|
|
4600
|
-
}
|
|
4601
|
-
this.relay.sendCommandAck(command.id, true, "Config reloaded");
|
|
4602
|
-
} catch (err) {
|
|
4603
|
-
console.error("[runtime] Config reload failed:", err.message);
|
|
4604
|
-
this.relay.sendCommandAck(command.id, false, err.message);
|
|
4605
|
-
}
|
|
4606
|
-
}
|
|
4607
|
-
// ── STATUS ─────────────────────────────────────────────────
|
|
4608
|
-
sendStatus() {
|
|
4609
|
-
const status = {
|
|
4610
|
-
mode: this.mode,
|
|
4611
|
-
agentId: this.config.agentId,
|
|
4612
|
-
walletAddress: this.walletAddress || void 0,
|
|
4613
|
-
sdkVersion: SDK_VERSION,
|
|
4614
|
-
cycleCount: this.cycleCount,
|
|
4615
|
-
lastCycleAt: this.lastCycleAt,
|
|
4616
|
-
tradingIntervalMs: this.config.trading.tradingIntervalMs,
|
|
4617
|
-
llm: {
|
|
4618
|
-
provider: this.config.llm.provider,
|
|
4619
|
-
model: this.config.llm.model || "default"
|
|
4620
|
-
},
|
|
4621
|
-
risk: {
|
|
4622
|
-
dailyPnL: this.risk.getDailyPnL(),
|
|
4623
|
-
dailyLossLimit: this.risk.getDailyLossLimit(),
|
|
4624
|
-
isLimitHit: this.risk.isDailyLossLimitHit()
|
|
4625
|
-
},
|
|
4626
|
-
positions: {
|
|
4627
|
-
openPositions: this.positions.getPositions().length,
|
|
4628
|
-
totalUnrealizedPnL: 0,
|
|
4629
|
-
totalRealizedPnL: this.positions.getRealizedPnL()
|
|
4630
|
-
},
|
|
4631
|
-
venues: {
|
|
4632
|
-
hyperliquid: {
|
|
4633
|
-
enabled: !!this.hlClient,
|
|
4634
|
-
trading: !!this.hlOrders && this.mode === "trading"
|
|
4635
|
-
},
|
|
4636
|
-
polymarket: {
|
|
4637
|
-
enabled: !!this.pmClient,
|
|
4638
|
-
trading: !!this.pmOrders && this.mode === "trading"
|
|
4639
|
-
},
|
|
4640
|
-
spot: {
|
|
4641
|
-
enabled: !!this.spotManager,
|
|
4642
|
-
trading: !!this.spotManager && this.mode === "trading"
|
|
4643
|
-
},
|
|
4644
|
-
bridge: {
|
|
4645
|
-
enabled: !!this.bridgeManager
|
|
4646
|
-
}
|
|
4647
|
-
}
|
|
4648
|
-
};
|
|
4649
|
-
if (this.paper) {
|
|
4650
|
-
const metrics = this.paper.getMetrics();
|
|
4651
|
-
status.paper = {
|
|
4652
|
-
active: true,
|
|
4653
|
-
simulatedValue: this.paper.getEquity(),
|
|
4654
|
-
simulatedPnLPct: metrics.totalReturn * 100
|
|
4655
|
-
};
|
|
4656
|
-
status.portfolioValue = this.paper.getEquity();
|
|
4657
|
-
}
|
|
4658
|
-
this.relay.sendHeartbeat(status);
|
|
4659
|
-
}
|
|
4660
|
-
};
|
|
4661
|
-
|
|
4662
|
-
export {
|
|
4663
|
-
loadConfig,
|
|
4664
|
-
generateSampleConfig,
|
|
4665
|
-
writeSampleConfig,
|
|
4666
|
-
CHAIN_CONFIGS,
|
|
4667
|
-
getChainConfig,
|
|
4668
|
-
DEFAULT_SPOT_CONFIG,
|
|
4669
|
-
RelayClient,
|
|
4670
|
-
SignalReporter,
|
|
4671
|
-
FileStore,
|
|
4672
|
-
PositionTracker,
|
|
4673
|
-
createLLMAdapter,
|
|
4674
|
-
getTemplate,
|
|
4675
|
-
listTemplates,
|
|
4676
|
-
loadStrategy,
|
|
4677
|
-
RiskManager,
|
|
4678
|
-
MarketDataService,
|
|
4679
|
-
PaperExecutor,
|
|
4680
|
-
HyperliquidClient,
|
|
4681
|
-
HYPERLIQUID_DOMAIN,
|
|
4682
|
-
HYPERLIQUID_USER_DOMAIN,
|
|
4683
|
-
USER_ACTION_TYPES,
|
|
4684
|
-
getNextNonce,
|
|
4685
|
-
HyperliquidSigner,
|
|
4686
|
-
HyperliquidOrderManager,
|
|
4687
|
-
HyperliquidPositionManager,
|
|
4688
|
-
HyperliquidWebSocket,
|
|
4689
|
-
encodePredictionInstrument,
|
|
4690
|
-
decodePredictionInstrument,
|
|
4691
|
-
PolymarketClient,
|
|
4692
|
-
PolymarketOrderManager,
|
|
4693
|
-
SpotDEXClient,
|
|
4694
|
-
UniswapAdapter,
|
|
4695
|
-
AerodromeAdapter,
|
|
4696
|
-
SpotSwapManager,
|
|
4697
|
-
DEFAULT_BRIDGE_CONFIG,
|
|
4698
|
-
AcrossAdapter,
|
|
4699
|
-
BridgeManager,
|
|
4700
|
-
AgentRuntime
|
|
4701
|
-
};
|