@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.
- package/dist/{chat-7CB4YGGP.js → chat-MUUC5L5W.js} +7 -7
- package/dist/{chunk-TWX6FSCM.js → chunk-B4BMCXWK.js} +11 -1
- package/dist/chunk-B4BMCXWK.js.map +1 -0
- package/dist/chunk-DCT3IDBS.js +458 -0
- package/dist/chunk-DCT3IDBS.js.map +1 -0
- package/dist/{chunk-MJMWA4LY.js → chunk-FEDSWXSD.js} +2 -2
- package/dist/{chunk-FR4LYDPJ.js → chunk-G2RQLZZI.js} +115 -4
- package/dist/chunk-G2RQLZZI.js.map +1 -0
- package/dist/{chunk-L24NGLKY.js → chunk-HRA2KPGW.js} +8 -3
- package/dist/{chunk-L24NGLKY.js.map → chunk-HRA2KPGW.js.map} +1 -1
- package/dist/{chunk-ARD44YTT.js → chunk-VZZ2V6EM.js} +5 -5
- package/dist/{chunk-TPE6ZTUI.js → chunk-W76CHVD3.js} +59 -5
- package/dist/chunk-W76CHVD3.js.map +1 -0
- package/dist/{chunk-5ZC2A7UP.js → chunk-WHCXQBPS.js} +4 -4
- package/dist/{chunk-Z2PNK3CC.js → chunk-ZXV4TBPE.js} +2 -2
- package/dist/client-I56MIQAM.js +21 -0
- package/dist/{config-LW4Q6NK5.js → config-2VMLHIXD.js} +6 -2
- package/dist/eas-YZF6MN65.js +29 -0
- package/dist/{governor-E6AU3UWV.js → governor-ZWKGLGMG.js} +6 -6
- package/dist/index.js +70 -296
- package/dist/index.js.map +1 -1
- package/dist/{network-C32G5D3J.js → network-3MVRM7O4.js} +3 -3
- package/dist/research-HC2UOLFT.js +14 -0
- package/dist/research-HC2UOLFT.js.map +1 -0
- package/dist/{research-MKI4RS2F.js → research-VZKLOTMU.js} +7 -7
- package/dist/{session-RAFLL5BD.js → session-X3QFFCJ7.js} +10 -10
- package/dist/trade-YCBFXOB3.js +874 -0
- package/dist/trade-YCBFXOB3.js.map +1 -0
- package/dist/{xmtp-ATRMY76G.js → xmtp-IH57GYSR.js} +6 -6
- package/package.json +1 -1
- package/dist/chunk-FR4LYDPJ.js.map +0 -1
- package/dist/chunk-TPE6ZTUI.js.map +0 -1
- package/dist/chunk-TWX6FSCM.js.map +0 -1
- package/dist/eas-DOC4QKDF.js +0 -23
- package/dist/research-V63URK4C.js +0 -14
- /package/dist/{chat-7CB4YGGP.js.map → chat-MUUC5L5W.js.map} +0 -0
- /package/dist/{chunk-MJMWA4LY.js.map → chunk-FEDSWXSD.js.map} +0 -0
- /package/dist/{chunk-ARD44YTT.js.map → chunk-VZZ2V6EM.js.map} +0 -0
- /package/dist/{chunk-5ZC2A7UP.js.map → chunk-WHCXQBPS.js.map} +0 -0
- /package/dist/{chunk-Z2PNK3CC.js.map → chunk-ZXV4TBPE.js.map} +0 -0
- /package/dist/{config-LW4Q6NK5.js.map → client-I56MIQAM.js.map} +0 -0
- /package/dist/{eas-DOC4QKDF.js.map → config-2VMLHIXD.js.map} +0 -0
- /package/dist/{governor-E6AU3UWV.js.map → eas-YZF6MN65.js.map} +0 -0
- /package/dist/{network-C32G5D3J.js.map → governor-ZWKGLGMG.js.map} +0 -0
- /package/dist/{research-V63URK4C.js.map → network-3MVRM7O4.js.map} +0 -0
- /package/dist/{research-MKI4RS2F.js.map → research-VZKLOTMU.js.map} +0 -0
- /package/dist/{session-RAFLL5BD.js.map → session-X3QFFCJ7.js.map} +0 -0
- /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
|