@sherwoodagent/cli 0.18.4 → 0.19.0

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.
Files changed (48) hide show
  1. package/dist/{chat-7CB4YGGP.js → chat-MUUC5L5W.js} +7 -7
  2. package/dist/{chunk-TWX6FSCM.js → chunk-B4BMCXWK.js} +11 -1
  3. package/dist/chunk-B4BMCXWK.js.map +1 -0
  4. package/dist/chunk-DCT3IDBS.js +458 -0
  5. package/dist/chunk-DCT3IDBS.js.map +1 -0
  6. package/dist/{chunk-MJMWA4LY.js → chunk-FEDSWXSD.js} +2 -2
  7. package/dist/{chunk-FR4LYDPJ.js → chunk-G2RQLZZI.js} +115 -4
  8. package/dist/chunk-G2RQLZZI.js.map +1 -0
  9. package/dist/{chunk-L24NGLKY.js → chunk-HRA2KPGW.js} +8 -3
  10. package/dist/{chunk-L24NGLKY.js.map → chunk-HRA2KPGW.js.map} +1 -1
  11. package/dist/{chunk-ARD44YTT.js → chunk-VZZ2V6EM.js} +5 -5
  12. package/dist/{chunk-TPE6ZTUI.js → chunk-W76CHVD3.js} +59 -5
  13. package/dist/chunk-W76CHVD3.js.map +1 -0
  14. package/dist/{chunk-5ZC2A7UP.js → chunk-WHCXQBPS.js} +4 -4
  15. package/dist/{chunk-Z2PNK3CC.js → chunk-ZXV4TBPE.js} +2 -2
  16. package/dist/client-I56MIQAM.js +21 -0
  17. package/dist/{config-LW4Q6NK5.js → config-2VMLHIXD.js} +6 -2
  18. package/dist/eas-YZF6MN65.js +29 -0
  19. package/dist/{governor-E6AU3UWV.js → governor-ZWKGLGMG.js} +6 -6
  20. package/dist/index.js +70 -296
  21. package/dist/index.js.map +1 -1
  22. package/dist/{network-C32G5D3J.js → network-3MVRM7O4.js} +3 -3
  23. package/dist/research-HC2UOLFT.js +14 -0
  24. package/dist/research-HC2UOLFT.js.map +1 -0
  25. package/dist/{research-MKI4RS2F.js → research-VZKLOTMU.js} +7 -7
  26. package/dist/{session-RAFLL5BD.js → session-X3QFFCJ7.js} +10 -10
  27. package/dist/trade-YCBFXOB3.js +874 -0
  28. package/dist/trade-YCBFXOB3.js.map +1 -0
  29. package/dist/{xmtp-ATRMY76G.js → xmtp-IH57GYSR.js} +6 -6
  30. package/package.json +1 -1
  31. package/dist/chunk-FR4LYDPJ.js.map +0 -1
  32. package/dist/chunk-TPE6ZTUI.js.map +0 -1
  33. package/dist/chunk-TWX6FSCM.js.map +0 -1
  34. package/dist/eas-DOC4QKDF.js +0 -23
  35. package/dist/research-V63URK4C.js +0 -14
  36. /package/dist/{chat-7CB4YGGP.js.map → chat-MUUC5L5W.js.map} +0 -0
  37. /package/dist/{chunk-MJMWA4LY.js.map → chunk-FEDSWXSD.js.map} +0 -0
  38. /package/dist/{chunk-ARD44YTT.js.map → chunk-VZZ2V6EM.js.map} +0 -0
  39. /package/dist/{chunk-5ZC2A7UP.js.map → chunk-WHCXQBPS.js.map} +0 -0
  40. /package/dist/{chunk-Z2PNK3CC.js.map → chunk-ZXV4TBPE.js.map} +0 -0
  41. /package/dist/{config-LW4Q6NK5.js.map → client-I56MIQAM.js.map} +0 -0
  42. /package/dist/{eas-DOC4QKDF.js.map → config-2VMLHIXD.js.map} +0 -0
  43. /package/dist/{governor-E6AU3UWV.js.map → eas-YZF6MN65.js.map} +0 -0
  44. /package/dist/{network-C32G5D3J.js.map → governor-ZWKGLGMG.js.map} +0 -0
  45. /package/dist/{research-V63URK4C.js.map → network-3MVRM7O4.js.map} +0 -0
  46. /package/dist/{research-MKI4RS2F.js.map → research-VZKLOTMU.js.map} +0 -0
  47. /package/dist/{session-RAFLL5BD.js.map → session-X3QFFCJ7.js.map} +0 -0
  48. /package/dist/{xmtp-ATRMY76G.js.map → xmtp-IH57GYSR.js.map} +0 -0
@@ -0,0 +1,874 @@
1
+ import {
2
+ getResearchProvider
3
+ } from "./chunk-ZXV4TBPE.js";
4
+ import {
5
+ UniswapProvider,
6
+ chatCompletion,
7
+ getQuote
8
+ } from "./chunk-DCT3IDBS.js";
9
+ import {
10
+ ERC20_ABI,
11
+ TOKENS
12
+ } from "./chunk-W76CHVD3.js";
13
+ import {
14
+ getAccount,
15
+ getPublicClient
16
+ } from "./chunk-HRA2KPGW.js";
17
+ import {
18
+ getExplorerUrl
19
+ } from "./chunk-FEDSWXSD.js";
20
+ import {
21
+ loadConfig,
22
+ saveConfig
23
+ } from "./chunk-B4BMCXWK.js";
24
+
25
+ // src/commands/trade.ts
26
+ import chalk from "chalk";
27
+ import ora from "ora";
28
+ import { confirm } from "@inquirer/prompts";
29
+ import { isAddress, parseUnits as parseUnits2, formatUnits as formatUnits2 } from "viem";
30
+
31
+ // src/lib/signals.ts
32
+ var DEFAULT_CONFIG = {
33
+ buyThreshold: 0.3,
34
+ sellThreshold: -0.2
35
+ };
36
+ async function analyzeToken(tokenAddress, tokenSymbol, config) {
37
+ const cfg = { ...DEFAULT_CONFIG, ...config };
38
+ let totalCostUsdc = 0;
39
+ const [onChain, social, fundamental] = await Promise.allSettled([
40
+ getOnChainSignal(tokenSymbol),
41
+ getSocialSignal(tokenAddress, tokenSymbol),
42
+ getFundamentalSignal(tokenSymbol)
43
+ ]);
44
+ const signals = [];
45
+ if (onChain.status === "fulfilled") {
46
+ signals.push(onChain.value.signal);
47
+ totalCostUsdc += onChain.value.costUsdc;
48
+ }
49
+ if (social.status === "fulfilled") {
50
+ signals.push(social.value.signal);
51
+ totalCostUsdc += social.value.costUsdc;
52
+ }
53
+ if (fundamental.status === "fulfilled") {
54
+ signals.push(fundamental.value.signal);
55
+ totalCostUsdc += fundamental.value.costUsdc;
56
+ }
57
+ let weightedSum = 0;
58
+ let totalWeight = 0;
59
+ for (const s of signals) {
60
+ weightedSum += s.value * s.weight;
61
+ totalWeight += s.weight;
62
+ }
63
+ const compositeScore = totalWeight > 0 ? weightedSum / totalWeight : 0;
64
+ const confidence = signals.length > 0 ? signals.reduce((sum, s) => sum + Math.abs(s.value), 0) / signals.length : 0;
65
+ let action = "hold";
66
+ if (compositeScore >= cfg.buyThreshold && confidence >= 0.5) {
67
+ action = "buy";
68
+ } else if (compositeScore <= cfg.sellThreshold) {
69
+ action = "sell";
70
+ }
71
+ return {
72
+ action,
73
+ confidence: Math.min(confidence, 1),
74
+ compositeScore,
75
+ signals,
76
+ costUsdc: totalCostUsdc.toFixed(4),
77
+ timestamp: Math.floor(Date.now() / 1e3)
78
+ };
79
+ }
80
+ async function getOnChainSignal(tokenSymbol) {
81
+ const nansen = getResearchProvider("nansen");
82
+ let result;
83
+ try {
84
+ result = await nansen.query({ type: "smart-money", target: tokenSymbol });
85
+ } catch {
86
+ return {
87
+ signal: {
88
+ source: "onchain",
89
+ name: "smart_money_net_flow",
90
+ value: 0,
91
+ weight: 0.4,
92
+ raw: { error: "nansen query failed" }
93
+ },
94
+ costUsdc: 0
95
+ };
96
+ }
97
+ const data = result.data;
98
+ const netFlow = extractNetFlow(data);
99
+ const value = Math.max(-1, Math.min(1, netFlow));
100
+ return {
101
+ signal: {
102
+ source: "onchain",
103
+ name: "smart_money_net_flow",
104
+ value,
105
+ weight: 0.4,
106
+ raw: data
107
+ },
108
+ costUsdc: Number(result.costUsdc) || 0.06
109
+ };
110
+ }
111
+ async function getSocialSignal(tokenAddress, tokenSymbol) {
112
+ try {
113
+ const result = await chatCompletion({
114
+ model: "llama-3.3-70b",
115
+ messages: [
116
+ {
117
+ role: "system",
118
+ content: `You are a crypto market sentiment analyst. Analyze current Twitter/X discourse about the given token. Return ONLY valid JSON with no markdown: {"sentiment": <number from -1.0 to 1.0>, "reasoning": "<brief explanation>", "tweetCount": <estimated tweets in last 24h>, "keyTopics": ["topic1", "topic2"]}. Positive sentiment means bullish discussion, influencer endorsements, positive news. Negative means FUD, rug pull warnings, community exodus. If you cannot find data, return {"sentiment": 0, "reasoning": "insufficient data", "tweetCount": 0, "keyTopics": []}.`
119
+ },
120
+ {
121
+ role: "user",
122
+ content: `Analyze current X/Twitter sentiment for token ${tokenSymbol} (contract: ${tokenAddress} on Base chain). What is the social consensus in the last 24 hours?`
123
+ }
124
+ ],
125
+ temperature: 0.3,
126
+ maxTokens: 500,
127
+ enableWebSearch: true
128
+ });
129
+ const parsed = parseJsonResponse(result.content);
130
+ const sentiment = typeof parsed.sentiment === "number" ? Math.max(-1, Math.min(1, parsed.sentiment)) : 0;
131
+ return {
132
+ signal: {
133
+ source: "social",
134
+ name: "x_sentiment",
135
+ value: sentiment,
136
+ weight: 0.3,
137
+ raw: parsed
138
+ },
139
+ costUsdc: 0
140
+ // Venice inference is prepaid via sVVV, no per-call cost
141
+ };
142
+ } catch {
143
+ return {
144
+ signal: {
145
+ source: "social",
146
+ name: "x_sentiment",
147
+ value: 0,
148
+ weight: 0.3,
149
+ raw: { error: "venice inference failed" }
150
+ },
151
+ costUsdc: 0
152
+ };
153
+ }
154
+ }
155
+ async function getFundamentalSignal(tokenSymbol) {
156
+ const messari = getResearchProvider("messari");
157
+ let result;
158
+ try {
159
+ result = await messari.query({ type: "market", target: tokenSymbol });
160
+ } catch {
161
+ return {
162
+ signal: {
163
+ source: "fundamental",
164
+ name: "market_fundamentals",
165
+ value: 0,
166
+ weight: 0.3,
167
+ raw: { error: "messari query failed" }
168
+ },
169
+ costUsdc: 0
170
+ };
171
+ }
172
+ const data = result.data;
173
+ const value = scoreFundamentals(data);
174
+ return {
175
+ signal: {
176
+ source: "fundamental",
177
+ name: "market_fundamentals",
178
+ value,
179
+ weight: 0.3,
180
+ raw: data
181
+ },
182
+ costUsdc: Number(result.costUsdc) || 0.2
183
+ };
184
+ }
185
+ function extractNetFlow(data) {
186
+ const netFlow = data.netFlow ?? data.net_flow ?? (data.inflow ?? 0) - (data.outflow ?? 0);
187
+ if (typeof netFlow !== "number" || isNaN(netFlow)) return 0;
188
+ const normalized = netFlow / 1e6;
189
+ return Math.max(-1, Math.min(1, normalized));
190
+ }
191
+ function scoreFundamentals(data) {
192
+ let score = 0;
193
+ const marketData = data.marketData ?? data.market_data ?? data;
194
+ const athData = data.allTimeHigh ?? data.ath ?? {};
195
+ const vol24h = toNumber(marketData.volume_last_24_hours ?? marketData.volume24h);
196
+ const vol7d = toNumber(marketData.volume_last_7_days ?? marketData.volume7d);
197
+ if (vol24h > 0 && vol7d > 0) {
198
+ const dailyAvg7d = vol7d / 7;
199
+ const ratio = vol24h / dailyAvg7d;
200
+ if (ratio > 3) score += 0.5;
201
+ else if (ratio > 2) score += 0.25;
202
+ else if (ratio < 0.5) score -= 0.25;
203
+ }
204
+ const mcap = toNumber(marketData.current_marketcap_usd ?? marketData.marketCap);
205
+ if (mcap > 0 && mcap < 1e7 && vol24h > 0) {
206
+ score += 0.3;
207
+ } else if (mcap > 1e9) {
208
+ score -= 0.15;
209
+ }
210
+ const currentPrice = toNumber(marketData.price_usd ?? marketData.currentPrice);
211
+ const athPrice = toNumber(athData.price ?? athData.athPrice);
212
+ if (currentPrice > 0 && athPrice > 0) {
213
+ const athDistance = (athPrice - currentPrice) / athPrice * 100;
214
+ if (athDistance > 80) score += 0.2;
215
+ else if (athDistance < 10) score -= 0.3;
216
+ }
217
+ return Math.max(-1, Math.min(1, score));
218
+ }
219
+ function toNumber(v) {
220
+ if (typeof v === "number") return v;
221
+ if (typeof v === "string") return Number(v) || 0;
222
+ return 0;
223
+ }
224
+ function parseJsonResponse(content) {
225
+ let cleaned = content.trim();
226
+ if (cleaned.startsWith("```")) {
227
+ cleaned = cleaned.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "");
228
+ }
229
+ const start = cleaned.indexOf("{");
230
+ const end = cleaned.lastIndexOf("}");
231
+ if (start >= 0 && end > start) {
232
+ try {
233
+ return JSON.parse(cleaned.slice(start, end + 1));
234
+ } catch {
235
+ }
236
+ }
237
+ return { sentiment: 0, reasoning: "failed to parse response", raw: content };
238
+ }
239
+
240
+ // src/lib/exit-strategy.ts
241
+ var DEFAULT_EXIT_CONFIG = {
242
+ stopLossPct: 10,
243
+ trailingStopPct: 0,
244
+ takeProfitPct: 0,
245
+ deadlineUnix: 0,
246
+ signalExitEnabled: true
247
+ };
248
+ function checkExit(entryPrice, currentPrice, highWaterPrice, config, signalResult) {
249
+ if (entryPrice <= 0) {
250
+ return { shouldExit: false, reason: "hold", currentPnlPct: 0, highWaterPnlPct: 0 };
251
+ }
252
+ const currentPnlPct = (currentPrice - entryPrice) / entryPrice * 100;
253
+ const highWaterPnlPct = (highWaterPrice - entryPrice) / entryPrice * 100;
254
+ if (config.deadlineUnix > 0 && Date.now() / 1e3 > config.deadlineUnix) {
255
+ return { shouldExit: true, reason: "deadline", currentPnlPct, highWaterPnlPct };
256
+ }
257
+ if (currentPnlPct <= -config.stopLossPct) {
258
+ return { shouldExit: true, reason: "stop_loss", currentPnlPct, highWaterPnlPct };
259
+ }
260
+ if (config.trailingStopPct > 0 && highWaterPrice > 0) {
261
+ const drawdownPct = (highWaterPrice - currentPrice) / highWaterPrice * 100;
262
+ if (drawdownPct >= config.trailingStopPct) {
263
+ return { shouldExit: true, reason: "trailing_stop", currentPnlPct, highWaterPnlPct };
264
+ }
265
+ }
266
+ if (config.takeProfitPct > 0 && currentPnlPct >= config.takeProfitPct) {
267
+ return { shouldExit: true, reason: "take_profit", currentPnlPct, highWaterPnlPct };
268
+ }
269
+ if (config.signalExitEnabled && signalResult && signalResult.action === "sell" && signalResult.confidence > 0.4) {
270
+ return { shouldExit: true, reason: "signal_bearish", currentPnlPct, highWaterPnlPct };
271
+ }
272
+ return { shouldExit: false, reason: "hold", currentPnlPct, highWaterPnlPct };
273
+ }
274
+
275
+ // src/lib/positions.ts
276
+ import { formatUnits, parseUnits } from "viem";
277
+ function getOpenPositions() {
278
+ const config = loadConfig();
279
+ return config.positions ?? [];
280
+ }
281
+ function addPosition(pos) {
282
+ const config = loadConfig();
283
+ const positions = config.positions ?? [];
284
+ positions.push(pos);
285
+ config.positions = positions;
286
+ saveConfig(config);
287
+ }
288
+ function closePosition(tokenAddress, exitData) {
289
+ const config = loadConfig();
290
+ const positions = config.positions ?? [];
291
+ const idx = positions.findIndex(
292
+ (p) => p.tokenAddress.toLowerCase() === tokenAddress.toLowerCase()
293
+ );
294
+ if (idx === -1) {
295
+ throw new Error(`No open position for ${tokenAddress}`);
296
+ }
297
+ const [pos] = positions.splice(idx, 1);
298
+ const closed = { ...pos, ...exitData };
299
+ const closedPositions = config.closedPositions ?? [];
300
+ closedPositions.push(closed);
301
+ config.positions = positions;
302
+ config.closedPositions = closedPositions;
303
+ saveConfig(config);
304
+ }
305
+ function updateHighWater(tokenAddress, price) {
306
+ const config = loadConfig();
307
+ const positions = config.positions ?? [];
308
+ const pos = positions.find(
309
+ (p) => p.tokenAddress.toLowerCase() === tokenAddress.toLowerCase()
310
+ );
311
+ if (pos && price > pos.highWaterPrice) {
312
+ pos.highWaterPrice = price;
313
+ config.positions = positions;
314
+ saveConfig(config);
315
+ }
316
+ }
317
+ async function getCurrentPrice(tokenAddress, tokenDecimals, feeTier) {
318
+ const oneToken = parseUnits("1", tokenDecimals);
319
+ const usdc = TOKENS().USDC;
320
+ const { amountOut } = await getQuote({
321
+ tokenIn: tokenAddress,
322
+ tokenOut: usdc,
323
+ amountIn: oneToken,
324
+ fee: feeTier
325
+ });
326
+ return Number(formatUnits(amountOut, 6));
327
+ }
328
+
329
+ // src/commands/trade.ts
330
+ var uniswap = new UniswapProvider();
331
+ async function loadXmtp() {
332
+ return import("./xmtp-IH57GYSR.js");
333
+ }
334
+ var KNOWN_MEMECOINS = {
335
+ DEGEN: "0x4ed4E862860beD51a9570b96d89aF5E1B0Efefed",
336
+ TOSHI: "0xAC1Bd2486aAf3B5C0fc3Fd868558b082a531B2B4",
337
+ BRETT: "0x532f27101965dd16442E59d40670FaF5eBB142E4",
338
+ HIGHER: "0x0578d8A44db98B23BF096A382e016e29a5Ce0ffe"
339
+ };
340
+ async function resolveToken(symbolOrAddress) {
341
+ let address;
342
+ let symbol;
343
+ if (isAddress(symbolOrAddress)) {
344
+ address = symbolOrAddress;
345
+ const client2 = getPublicClient();
346
+ try {
347
+ symbol = await client2.readContract({
348
+ address,
349
+ abi: ERC20_ABI,
350
+ functionName: "symbol"
351
+ });
352
+ } catch {
353
+ symbol = address.slice(0, 8);
354
+ }
355
+ } else {
356
+ const upper = symbolOrAddress.toUpperCase();
357
+ const tokens = TOKENS();
358
+ const tokenMap = {
359
+ USDC: tokens.USDC,
360
+ WETH: tokens.WETH,
361
+ ...KNOWN_MEMECOINS
362
+ };
363
+ address = tokenMap[upper];
364
+ if (!address) {
365
+ throw new Error(
366
+ `Unknown token: ${symbolOrAddress}. Use a contract address or known symbol (${Object.keys(tokenMap).join(", ")}).`
367
+ );
368
+ }
369
+ symbol = upper;
370
+ }
371
+ const client = getPublicClient();
372
+ const decimals = await client.readContract({
373
+ address,
374
+ abi: ERC20_ABI,
375
+ functionName: "decimals"
376
+ });
377
+ return { address, symbol, decimals };
378
+ }
379
+ async function postToChat(syndicate, type, text, data) {
380
+ try {
381
+ const xmtp = await loadXmtp();
382
+ const group = await xmtp.getGroup("", syndicate);
383
+ await xmtp.sendEnvelope(group, {
384
+ type,
385
+ from: getAccount().address,
386
+ text,
387
+ data,
388
+ timestamp: Math.floor(Date.now() / 1e3)
389
+ });
390
+ } catch {
391
+ }
392
+ }
393
+ function formatSignalTable(results) {
394
+ console.log();
395
+ console.log(chalk.bold(" Token Score Action Conf. On-chain Social Fundmtl"));
396
+ console.log(chalk.dim(" " + "\u2500".repeat(72)));
397
+ for (const { symbol, address, result } of results) {
398
+ const scoreColor = result.compositeScore > 0 ? chalk.green : result.compositeScore < 0 ? chalk.red : chalk.dim;
399
+ const actionColor = result.action === "buy" ? chalk.green.bold : result.action === "sell" ? chalk.red.bold : chalk.dim;
400
+ const onChain = result.signals.find((s) => s.source === "onchain");
401
+ const social = result.signals.find((s) => s.source === "social");
402
+ const fundamental = result.signals.find((s) => s.source === "fundamental");
403
+ const shortAddr = `${address.slice(0, 6)}..`;
404
+ const label = `${symbol.padEnd(6)} (${shortAddr})`;
405
+ console.log(
406
+ ` ${label.padEnd(16)} ${scoreColor(result.compositeScore.toFixed(2).padStart(6))} ${actionColor(result.action.toUpperCase().padEnd(6))} ${(result.confidence * 100).toFixed(0).padStart(4)}% ${formatSignalValue(onChain?.value).padStart(8)} ${formatSignalValue(social?.value).padStart(6)} ${formatSignalValue(fundamental?.value).padStart(7)}`
407
+ );
408
+ }
409
+ console.log();
410
+ }
411
+ function formatSignalValue(v) {
412
+ if (v === void 0) return chalk.dim("n/a");
413
+ const s = (v >= 0 ? "+" : "") + v.toFixed(2);
414
+ return v > 0 ? chalk.green(s) : v < 0 ? chalk.red(s) : chalk.dim(s);
415
+ }
416
+ function registerTradeCommands(program) {
417
+ const trade = program.command("trade").description("Memecoin trading \u2014 scan, buy, sell, monitor positions (Uniswap Trading API on Base)");
418
+ trade.command("scan").description("Analyze token(s) using on-chain, social, and fundamental signals").option("--token <addr|symbol>", "Specific token to analyze (otherwise scans known memecoins)").option("--syndicate <name>", "Post results to syndicate chat").option("--yes", "Skip cost confirmation", false).action(async (opts) => {
419
+ const targets = [];
420
+ if (opts.token) {
421
+ const resolved = await resolveToken(opts.token);
422
+ targets.push({ symbol: resolved.symbol, address: resolved.address });
423
+ } else {
424
+ for (const [symbol, address] of Object.entries(KNOWN_MEMECOINS)) {
425
+ targets.push({ symbol, address });
426
+ }
427
+ }
428
+ const estCost = targets.length * 0.26;
429
+ console.log();
430
+ console.log(chalk.bold("Memecoin Alpha Scan"));
431
+ console.log(chalk.dim("\u2500".repeat(40)));
432
+ console.log(` Tokens: ${targets.map((t) => t.symbol).join(", ")}`);
433
+ console.log(` Est. cost: ${chalk.yellow(`~$${estCost.toFixed(2)} USDC`)} (x402 research) + Venice inference`);
434
+ console.log();
435
+ if (!opts.yes) {
436
+ const ok = await confirm({
437
+ message: `Proceed with signal analysis?`,
438
+ default: true
439
+ });
440
+ if (!ok) {
441
+ console.log(chalk.dim("Cancelled."));
442
+ return;
443
+ }
444
+ }
445
+ const spinner = ora("Analyzing signals...").start();
446
+ const results = [];
447
+ for (const target of targets) {
448
+ try {
449
+ spinner.text = `Analyzing ${target.symbol}...`;
450
+ const result = await analyzeToken(target.address, target.symbol);
451
+ results.push({ ...target, result });
452
+ } catch {
453
+ results.push({
454
+ ...target,
455
+ result: {
456
+ action: "hold",
457
+ confidence: 0,
458
+ compositeScore: 0,
459
+ signals: [],
460
+ costUsdc: "0",
461
+ timestamp: Math.floor(Date.now() / 1e3)
462
+ }
463
+ });
464
+ }
465
+ }
466
+ spinner.succeed("Scan complete");
467
+ formatSignalTable(results);
468
+ const totalCost = results.reduce((sum, r) => sum + Number(r.result.costUsdc), 0);
469
+ console.log(chalk.dim(` Total research cost: $${totalCost.toFixed(4)} USDC`));
470
+ console.log();
471
+ if (opts.syndicate) {
472
+ await postToChat(
473
+ opts.syndicate,
474
+ "TRADE_SIGNAL",
475
+ `Scanned ${results.length} tokens: ${results.filter((r) => r.result.action === "buy").map((r) => r.symbol).join(", ") || "no buys"}`,
476
+ { results: results.map((r) => ({ symbol: r.symbol, action: r.result.action, score: r.result.compositeScore })) }
477
+ );
478
+ }
479
+ });
480
+ trade.command("buy").description("Buy a token with USDC via Uniswap Trading API").requiredOption("--token <addr|symbol>", "Token to buy").requiredOption("--amount <usdc>", "USDC amount to spend").option("--slippage <pct>", "Slippage tolerance % (default: 0.5)", "0.5").option("--stop-loss <pct>", "Stop loss percentage (default: 10)", "10").option("--trailing-stop <pct>", "Trailing stop percentage (0 = disabled)", "0").option("--deadline <hours>", "Force exit after N hours (0 = none)", "0").option("--syndicate <name>", "Post trade to syndicate chat").action(async (opts) => {
481
+ const { address: tokenAddr, symbol, decimals } = await resolveToken(opts.token);
482
+ const usdc = TOKENS().USDC;
483
+ const usdcAmount = parseUnits2(opts.amount, 6);
484
+ const slippage = Number(opts.slippage);
485
+ const client = getPublicClient();
486
+ const account = getAccount();
487
+ const balance = await client.readContract({
488
+ address: usdc,
489
+ abi: ERC20_ABI,
490
+ functionName: "balanceOf",
491
+ args: [account.address]
492
+ });
493
+ if (balance < usdcAmount) {
494
+ console.error(chalk.red(
495
+ `Insufficient USDC. Have ${formatUnits2(balance, 6)}, need ${opts.amount}`
496
+ ));
497
+ process.exit(1);
498
+ }
499
+ const quoteSpinner = ora("Getting quote from Uniswap API...").start();
500
+ let expectedOut;
501
+ try {
502
+ const result = await uniswap.fullQuote({
503
+ tokenIn: usdc,
504
+ tokenOut: tokenAddr,
505
+ amountIn: usdcAmount,
506
+ slippageTolerance: slippage
507
+ });
508
+ expectedOut = result.amountOut;
509
+ const routing = result.routing;
510
+ quoteSpinner.succeed(
511
+ `Quote: ${formatUnits2(result.amountOut, decimals)} ${symbol} (${routing} route)`
512
+ );
513
+ } catch (err) {
514
+ quoteSpinner.fail("Quote failed");
515
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
516
+ process.exit(1);
517
+ }
518
+ const swapSpinner = ora("Executing swap via Uniswap API...").start();
519
+ let txHash;
520
+ try {
521
+ const result = await uniswap.swap({
522
+ tokenIn: usdc,
523
+ tokenOut: tokenAddr,
524
+ amountIn: usdcAmount,
525
+ amountOutMinimum: 0n,
526
+ // slippage handled by API
527
+ fee: 3e3
528
+ // unused in API mode
529
+ });
530
+ txHash = result.hash;
531
+ if (!result.success) {
532
+ swapSpinner.fail("Swap reverted");
533
+ process.exit(1);
534
+ }
535
+ swapSpinner.succeed("Swap executed");
536
+ } catch (err) {
537
+ swapSpinner.fail("Swap failed");
538
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
539
+ process.exit(1);
540
+ }
541
+ const tokenBalance = await client.readContract({
542
+ address: tokenAddr,
543
+ abi: ERC20_ABI,
544
+ functionName: "balanceOf",
545
+ args: [account.address]
546
+ });
547
+ const tokensReceived = tokenBalance > 0n ? tokenBalance : expectedOut;
548
+ const entryPrice = Number(opts.amount) / Number(formatUnits2(tokensReceived, decimals));
549
+ const exitConfig = {
550
+ stopLossPct: Number(opts.stopLoss) || DEFAULT_EXIT_CONFIG.stopLossPct,
551
+ trailingStopPct: Number(opts.trailingStop) || DEFAULT_EXIT_CONFIG.trailingStopPct,
552
+ takeProfitPct: DEFAULT_EXIT_CONFIG.takeProfitPct,
553
+ deadlineUnix: Number(opts.deadline) > 0 ? Math.floor(Date.now() / 1e3) + Number(opts.deadline) * 3600 : 0,
554
+ signalExitEnabled: true
555
+ };
556
+ addPosition({
557
+ tokenAddress: tokenAddr,
558
+ tokenSymbol: symbol,
559
+ tokenDecimals: decimals,
560
+ amountIn: opts.amount,
561
+ amountOut: tokensReceived.toString(),
562
+ entryPrice,
563
+ highWaterPrice: entryPrice,
564
+ feeTier: 3e3,
565
+ // stored for price lookups via QuoterV2
566
+ openedAt: Math.floor(Date.now() / 1e3),
567
+ txHash,
568
+ exitConfig
569
+ });
570
+ console.log();
571
+ console.log(chalk.bold("Trade Executed"));
572
+ console.log(chalk.dim("\u2500".repeat(40)));
573
+ console.log(` Token: ${symbol} (${tokenAddr})`);
574
+ console.log(` Spent: ${opts.amount} USDC`);
575
+ console.log(` Received: ${formatUnits2(tokensReceived, decimals)} ${symbol}`);
576
+ console.log(` Entry: $${entryPrice.toFixed(8)} per ${symbol}`);
577
+ console.log(` Stop: -${exitConfig.stopLossPct}%`);
578
+ if (exitConfig.trailingStopPct > 0) {
579
+ console.log(` Trailing: ${exitConfig.trailingStopPct}%`);
580
+ }
581
+ if (exitConfig.deadlineUnix > 0) {
582
+ console.log(` Deadline: ${new Date(exitConfig.deadlineUnix * 1e3).toISOString()}`);
583
+ }
584
+ console.log(` Tx: ${chalk.dim(getExplorerUrl(txHash))}`);
585
+ try {
586
+ const { createTradeAttestation, getEasScanUrl } = await import("./eas-YZF6MN65.js");
587
+ const { uid } = await createTradeAttestation(
588
+ usdc,
589
+ tokenAddr,
590
+ usdcAmount,
591
+ formatUnits2(tokensReceived, decimals),
592
+ txHash,
593
+ "BUY"
594
+ );
595
+ if (uid !== "0x0000000000000000000000000000000000000000000000000000000000000000") {
596
+ console.log(` Attested: ${chalk.dim(getEasScanUrl(uid))}`);
597
+ }
598
+ } catch {
599
+ }
600
+ console.log();
601
+ if (opts.syndicate) {
602
+ await postToChat(
603
+ opts.syndicate,
604
+ "TRADE_EXECUTED",
605
+ `Bought ${formatUnits2(tokensReceived, decimals)} ${symbol} for ${opts.amount} USDC via Uniswap`,
606
+ { token: symbol, address: tokenAddr, amountUsdc: opts.amount, txHash }
607
+ );
608
+ }
609
+ });
610
+ trade.command("sell").description("Sell a token position back to USDC via Uniswap Trading API").requiredOption("--token <addr|symbol>", "Token to sell").option("--amount <n>", "Token amount to sell (default: entire position)").option("--slippage <pct>", "Slippage tolerance % (default: 0.5)", "0.5").option("--syndicate <name>", "Post trade to syndicate chat").action(async (opts) => {
611
+ const { address: tokenAddr, symbol, decimals } = await resolveToken(opts.token);
612
+ const usdc = TOKENS().USDC;
613
+ const slippage = Number(opts.slippage);
614
+ const positions = getOpenPositions();
615
+ const pos = positions.find(
616
+ (p) => p.tokenAddress.toLowerCase() === tokenAddr.toLowerCase()
617
+ );
618
+ const client = getPublicClient();
619
+ const account = getAccount();
620
+ let sellAmount;
621
+ if (opts.amount) {
622
+ sellAmount = parseUnits2(opts.amount, decimals);
623
+ } else {
624
+ sellAmount = await client.readContract({
625
+ address: tokenAddr,
626
+ abi: ERC20_ABI,
627
+ functionName: "balanceOf",
628
+ args: [account.address]
629
+ });
630
+ }
631
+ if (sellAmount === 0n) {
632
+ console.error(chalk.red("No tokens to sell."));
633
+ process.exit(1);
634
+ }
635
+ const quoteSpinner = ora("Getting quote from Uniswap API...").start();
636
+ let expectedUsdc;
637
+ try {
638
+ const result = await uniswap.fullQuote({
639
+ tokenIn: tokenAddr,
640
+ tokenOut: usdc,
641
+ amountIn: sellAmount,
642
+ slippageTolerance: slippage
643
+ });
644
+ expectedUsdc = result.amountOut;
645
+ const routing = result.routing;
646
+ quoteSpinner.succeed(
647
+ `Quote: ${formatUnits2(result.amountOut, 6)} USDC (${routing} route)`
648
+ );
649
+ } catch (err) {
650
+ quoteSpinner.fail("Quote failed");
651
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
652
+ process.exit(1);
653
+ }
654
+ const swapSpinner = ora("Executing sell via Uniswap API...").start();
655
+ let txHash;
656
+ try {
657
+ const result = await uniswap.swap({
658
+ tokenIn: tokenAddr,
659
+ tokenOut: usdc,
660
+ amountIn: sellAmount,
661
+ amountOutMinimum: 0n,
662
+ fee: 3e3
663
+ });
664
+ txHash = result.hash;
665
+ if (!result.success) {
666
+ swapSpinner.fail("Sell reverted");
667
+ process.exit(1);
668
+ }
669
+ swapSpinner.succeed("Sell executed");
670
+ } catch (err) {
671
+ swapSpinner.fail("Sell failed");
672
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
673
+ process.exit(1);
674
+ }
675
+ const usdcReceived = Number(formatUnits2(expectedUsdc, 6));
676
+ const costBasis = pos ? Number(pos.amountIn) : 0;
677
+ const pnlUsdc = costBasis > 0 ? usdcReceived - costBasis : 0;
678
+ const pnlPct = costBasis > 0 ? pnlUsdc / costBasis * 100 : 0;
679
+ const exitPrice = sellAmount > 0n ? usdcReceived / Number(formatUnits2(sellAmount, decimals)) : 0;
680
+ if (pos) {
681
+ closePosition(tokenAddr, {
682
+ exitPrice,
683
+ closedAt: Math.floor(Date.now() / 1e3),
684
+ exitTxHash: txHash,
685
+ exitReason: "manual",
686
+ pnlUsdc,
687
+ pnlPct
688
+ });
689
+ }
690
+ const pnlColor = pnlUsdc >= 0 ? chalk.green : chalk.red;
691
+ console.log();
692
+ console.log(chalk.bold("Position Closed"));
693
+ console.log(chalk.dim("\u2500".repeat(40)));
694
+ console.log(` Token: ${symbol}`);
695
+ console.log(` Sold: ${formatUnits2(sellAmount, decimals)} ${symbol}`);
696
+ console.log(` Received: ~${usdcReceived.toFixed(2)} USDC`);
697
+ if (costBasis > 0) {
698
+ console.log(` Cost: ${costBasis.toFixed(2)} USDC`);
699
+ console.log(` P&L: ${pnlColor(`${pnlUsdc >= 0 ? "+" : ""}${pnlUsdc.toFixed(2)} USDC (${pnlPct >= 0 ? "+" : ""}${pnlPct.toFixed(1)}%)`)}`);
700
+ }
701
+ console.log(` Tx: ${chalk.dim(getExplorerUrl(txHash))}`);
702
+ try {
703
+ const { createTradeAttestation, getEasScanUrl } = await import("./eas-YZF6MN65.js");
704
+ const { uid } = await createTradeAttestation(
705
+ tokenAddr,
706
+ usdc,
707
+ sellAmount,
708
+ usdcReceived.toFixed(6),
709
+ txHash,
710
+ "SELL"
711
+ );
712
+ if (uid !== "0x0000000000000000000000000000000000000000000000000000000000000000") {
713
+ console.log(` Attested: ${chalk.dim(getEasScanUrl(uid))}`);
714
+ }
715
+ } catch {
716
+ }
717
+ console.log();
718
+ if (opts.syndicate) {
719
+ await postToChat(
720
+ opts.syndicate,
721
+ "TRADE_EXECUTED",
722
+ `Sold ${formatUnits2(sellAmount, decimals)} ${symbol} for ~${usdcReceived.toFixed(2)} USDC (P&L: ${pnlUsdc >= 0 ? "+" : ""}${pnlUsdc.toFixed(2)})`,
723
+ { token: symbol, address: tokenAddr, usdcReceived, pnlUsdc, pnlPct, txHash }
724
+ );
725
+ }
726
+ });
727
+ trade.command("positions").description("Show open positions with current prices and unrealized P&L").action(async () => {
728
+ const positions = getOpenPositions();
729
+ if (positions.length === 0) {
730
+ console.log(chalk.dim("\n No open positions.\n"));
731
+ return;
732
+ }
733
+ console.log();
734
+ console.log(chalk.bold("Open Positions"));
735
+ console.log(chalk.dim("\u2500".repeat(80)));
736
+ console.log(chalk.dim(
737
+ " Token Entry Current Qty Cost Value P&L"
738
+ ));
739
+ for (const pos of positions) {
740
+ try {
741
+ const current = await getCurrentPrice(pos.tokenAddress, pos.tokenDecimals, pos.feeTier);
742
+ const qty = Number(formatUnits2(BigInt(pos.amountOut), pos.tokenDecimals));
743
+ const cost = Number(pos.amountIn);
744
+ const value = qty * current;
745
+ const pnl = value - cost;
746
+ const pnlPct = cost > 0 ? pnl / cost * 100 : 0;
747
+ const pnlColor = pnl >= 0 ? chalk.green : chalk.red;
748
+ console.log(
749
+ ` ${pos.tokenSymbol.padEnd(14)} $${pos.entryPrice.toFixed(6).padStart(10)} $${current.toFixed(6).padStart(10)} ${qty.toFixed(2).padStart(15)} $${cost.toFixed(2).padStart(8)} $${value.toFixed(2).padStart(8)} ` + pnlColor(`${pnl >= 0 ? "+" : ""}${pnl.toFixed(2)} (${pnlPct >= 0 ? "+" : ""}${pnlPct.toFixed(1)}%)`)
750
+ );
751
+ if (current > pos.highWaterPrice) {
752
+ updateHighWater(pos.tokenAddress, current);
753
+ }
754
+ } catch {
755
+ console.log(
756
+ ` ${pos.tokenSymbol.padEnd(14)} $${pos.entryPrice.toFixed(6).padStart(10)} ${chalk.dim("price unavailable")}`
757
+ );
758
+ }
759
+ }
760
+ console.log();
761
+ });
762
+ trade.command("monitor").description("Monitor positions and auto-exit on signal triggers").option("--interval <seconds>", "Check interval in seconds (default: 300)", "300").option("--syndicate <name>", "Post updates to syndicate chat").action(async (opts) => {
763
+ const interval = Number(opts.interval) * 1e3;
764
+ console.log();
765
+ console.log(chalk.bold("Position Monitor"));
766
+ console.log(chalk.dim(`Checking every ${opts.interval}s. Press Ctrl-C to stop.`));
767
+ console.log();
768
+ const running = { value: true };
769
+ process.on("SIGINT", () => {
770
+ running.value = false;
771
+ console.log(chalk.dim("\nStopping monitor..."));
772
+ });
773
+ while (running.value) {
774
+ const positions = getOpenPositions();
775
+ if (positions.length === 0) {
776
+ console.log(chalk.dim(" No open positions. Waiting..."));
777
+ await sleep(interval);
778
+ continue;
779
+ }
780
+ for (const pos of positions) {
781
+ if (!running.value) break;
782
+ try {
783
+ const current = await getCurrentPrice(pos.tokenAddress, pos.tokenDecimals, pos.feeTier);
784
+ const hwPrice = Math.max(current, pos.highWaterPrice);
785
+ if (current > pos.highWaterPrice) {
786
+ updateHighWater(pos.tokenAddress, current);
787
+ }
788
+ let signalResult;
789
+ if (pos.exitConfig.signalExitEnabled) {
790
+ try {
791
+ signalResult = await analyzeToken(pos.tokenAddress, pos.tokenSymbol);
792
+ } catch {
793
+ }
794
+ }
795
+ const exit = checkExit(pos.entryPrice, current, hwPrice, pos.exitConfig, signalResult);
796
+ const pnlPct = exit.currentPnlPct;
797
+ const pnlColor = pnlPct >= 0 ? chalk.green : chalk.red;
798
+ const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString();
799
+ console.log(
800
+ ` [${ts}] ${pos.tokenSymbol}: $${current.toFixed(6)} ` + pnlColor(`(${pnlPct >= 0 ? "+" : ""}${pnlPct.toFixed(1)}%)`) + (exit.shouldExit ? chalk.red.bold(` \u2192 EXIT (${exit.reason})`) : "")
801
+ );
802
+ if (exit.shouldExit) {
803
+ console.log(chalk.yellow(` Executing exit for ${pos.tokenSymbol}: ${exit.reason}`));
804
+ if (opts.syndicate) {
805
+ await postToChat(
806
+ opts.syndicate,
807
+ "RISK_ALERT",
808
+ `Exit triggered for ${pos.tokenSymbol}: ${exit.reason} (${pnlPct >= 0 ? "+" : ""}${pnlPct.toFixed(1)}%)`,
809
+ { token: pos.tokenSymbol, reason: exit.reason, pnlPct }
810
+ );
811
+ }
812
+ try {
813
+ const sellAmount = BigInt(pos.amountOut);
814
+ const usdc = TOKENS().USDC;
815
+ const { amountOut: sellQuote } = await uniswap.fullQuote({
816
+ tokenIn: pos.tokenAddress,
817
+ tokenOut: usdc,
818
+ amountIn: sellAmount,
819
+ slippageTolerance: 0.5
820
+ });
821
+ const result = await uniswap.swap({
822
+ tokenIn: pos.tokenAddress,
823
+ tokenOut: usdc,
824
+ amountIn: sellAmount,
825
+ amountOutMinimum: 0n,
826
+ fee: 3e3
827
+ });
828
+ const usdcReceived = Number(formatUnits2(sellQuote, 6));
829
+ const costBasis = Number(pos.amountIn);
830
+ const pnlUsdc = usdcReceived - costBasis;
831
+ closePosition(pos.tokenAddress, {
832
+ exitPrice: current,
833
+ closedAt: Math.floor(Date.now() / 1e3),
834
+ exitTxHash: result.hash,
835
+ exitReason: exit.reason,
836
+ pnlUsdc,
837
+ pnlPct
838
+ });
839
+ const pnlStr = `${pnlUsdc >= 0 ? "+" : ""}${pnlUsdc.toFixed(2)} USDC`;
840
+ console.log(chalk.green(` Sold ${pos.tokenSymbol}: ${pnlStr}`));
841
+ if (opts.syndicate) {
842
+ await postToChat(
843
+ opts.syndicate,
844
+ "TRADE_EXECUTED",
845
+ `Auto-sold ${pos.tokenSymbol}: ${pnlStr} (${exit.reason})`,
846
+ { token: pos.tokenSymbol, reason: exit.reason, pnlUsdc, pnlPct, txHash: result.hash }
847
+ );
848
+ }
849
+ } catch (err) {
850
+ console.error(chalk.red(
851
+ ` Failed to sell ${pos.tokenSymbol}: ${err instanceof Error ? err.message : String(err)}`
852
+ ));
853
+ }
854
+ }
855
+ } catch {
856
+ console.error(chalk.dim(
857
+ ` [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] ${pos.tokenSymbol}: price check failed`
858
+ ));
859
+ }
860
+ }
861
+ if (running.value) {
862
+ await sleep(interval);
863
+ }
864
+ }
865
+ console.log(chalk.dim("Monitor stopped."));
866
+ });
867
+ }
868
+ function sleep(ms) {
869
+ return new Promise((resolve) => setTimeout(resolve, ms));
870
+ }
871
+ export {
872
+ registerTradeCommands
873
+ };
874
+ //# sourceMappingURL=trade-YCBFXOB3.js.map