@kernel.chat/kbot 3.23.0 → 3.26.1
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/README.md +33 -22
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +3 -1
- package/dist/agent.js.map +1 -1
- package/dist/agents/trader.d.ts +32 -0
- package/dist/agents/trader.d.ts.map +1 -0
- package/dist/agents/trader.js +190 -0
- package/dist/agents/trader.js.map +1 -0
- package/dist/cli.js +219 -15
- package/dist/cli.js.map +1 -1
- package/dist/context.d.ts +4 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +28 -1
- package/dist/context.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +71 -1
- package/dist/doctor.js.map +1 -1
- package/dist/inference.d.ts +9 -0
- package/dist/inference.d.ts.map +1 -1
- package/dist/inference.js +38 -5
- package/dist/inference.js.map +1 -1
- package/dist/introspection.d.ts +17 -0
- package/dist/introspection.d.ts.map +1 -0
- package/dist/introspection.js +490 -0
- package/dist/introspection.js.map +1 -0
- package/dist/learned-router.d.ts.map +1 -1
- package/dist/learned-router.js +3 -0
- package/dist/learned-router.js.map +1 -1
- package/dist/machine.d.ts +85 -0
- package/dist/machine.d.ts.map +1 -0
- package/dist/machine.js +538 -0
- package/dist/machine.js.map +1 -0
- package/dist/matrix.d.ts.map +1 -1
- package/dist/matrix.js +11 -0
- package/dist/matrix.js.map +1 -1
- package/dist/provider-fallback.d.ts +6 -0
- package/dist/provider-fallback.d.ts.map +1 -1
- package/dist/provider-fallback.js +29 -0
- package/dist/provider-fallback.js.map +1 -1
- package/dist/synthesis-engine.d.ts +175 -0
- package/dist/synthesis-engine.d.ts.map +1 -0
- package/dist/synthesis-engine.js +783 -0
- package/dist/synthesis-engine.js.map +1 -0
- package/dist/tool-pipeline.d.ts +7 -1
- package/dist/tool-pipeline.d.ts.map +1 -1
- package/dist/tool-pipeline.js +39 -1
- package/dist/tool-pipeline.js.map +1 -1
- package/dist/tools/finance.d.ts +2 -0
- package/dist/tools/finance.d.ts.map +1 -0
- package/dist/tools/finance.js +1116 -0
- package/dist/tools/finance.js.map +1 -0
- package/dist/tools/finance.test.d.ts +2 -0
- package/dist/tools/finance.test.d.ts.map +1 -0
- package/dist/tools/finance.test.js +245 -0
- package/dist/tools/finance.test.js.map +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +5 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/machine-tools.d.ts +2 -0
- package/dist/tools/machine-tools.d.ts.map +1 -0
- package/dist/tools/machine-tools.js +690 -0
- package/dist/tools/machine-tools.js.map +1 -0
- package/dist/tools/sentiment.d.ts +2 -0
- package/dist/tools/sentiment.d.ts.map +1 -0
- package/dist/tools/sentiment.js +513 -0
- package/dist/tools/sentiment.js.map +1 -0
- package/dist/tools/stocks.d.ts +2 -0
- package/dist/tools/stocks.d.ts.map +1 -0
- package/dist/tools/stocks.js +345 -0
- package/dist/tools/stocks.js.map +1 -0
- package/dist/tools/stocks.test.d.ts +2 -0
- package/dist/tools/stocks.test.d.ts.map +1 -0
- package/dist/tools/stocks.test.js +82 -0
- package/dist/tools/stocks.test.js.map +1 -0
- package/dist/tools/wallet.d.ts +2 -0
- package/dist/tools/wallet.d.ts.map +1 -0
- package/dist/tools/wallet.js +698 -0
- package/dist/tools/wallet.js.map +1 -0
- package/dist/tools/wallet.test.d.ts +2 -0
- package/dist/tools/wallet.test.d.ts.map +1 -0
- package/dist/tools/wallet.test.js +205 -0
- package/dist/tools/wallet.test.js.map +1 -0
- package/dist/ui.js +1 -1
- package/dist/ui.js.map +1 -1
- package/package.json +94 -42
|
@@ -0,0 +1,1116 @@
|
|
|
1
|
+
// kbot Finance Tools — Market data, technical analysis, paper trading, wallet queries
|
|
2
|
+
// All market data uses free APIs (CoinGecko, Yahoo). No auth required for read-only.
|
|
3
|
+
// Paper trading is local-only (~/.kbot/paper-portfolio.json).
|
|
4
|
+
// Real trading requires explicit wallet config + user confirmation.
|
|
5
|
+
import { registerTool } from './index.js';
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
const PORTFOLIO_DIR = join(homedir(), '.kbot');
|
|
10
|
+
const PORTFOLIO_PATH = join(PORTFOLIO_DIR, 'paper-portfolio.json');
|
|
11
|
+
function loadPortfolio() {
|
|
12
|
+
if (existsSync(PORTFOLIO_PATH)) {
|
|
13
|
+
return JSON.parse(readFileSync(PORTFOLIO_PATH, 'utf-8'));
|
|
14
|
+
}
|
|
15
|
+
const fresh = {
|
|
16
|
+
cash: 100_000,
|
|
17
|
+
positions: [],
|
|
18
|
+
trades: [],
|
|
19
|
+
createdAt: new Date().toISOString(),
|
|
20
|
+
limits: {
|
|
21
|
+
maxPositionPct: 25,
|
|
22
|
+
maxDailyLossPct: 5,
|
|
23
|
+
stopLossPct: 15,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
savePortfolio(fresh);
|
|
27
|
+
return fresh;
|
|
28
|
+
}
|
|
29
|
+
function savePortfolio(p) {
|
|
30
|
+
if (!existsSync(PORTFOLIO_DIR))
|
|
31
|
+
mkdirSync(PORTFOLIO_DIR, { recursive: true });
|
|
32
|
+
writeFileSync(PORTFOLIO_PATH, JSON.stringify(p, null, 2));
|
|
33
|
+
}
|
|
34
|
+
// ── Helpers ──
|
|
35
|
+
async function fetchJSON(url, timeout = 10_000) {
|
|
36
|
+
const res = await fetch(url, {
|
|
37
|
+
headers: { 'User-Agent': 'KBot/3.0 (Finance Tools)', Accept: 'application/json' },
|
|
38
|
+
signal: AbortSignal.timeout(timeout),
|
|
39
|
+
});
|
|
40
|
+
if (!res.ok)
|
|
41
|
+
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
42
|
+
return res.json();
|
|
43
|
+
}
|
|
44
|
+
function fmt(n, decimals = 2) {
|
|
45
|
+
return n.toLocaleString('en-US', { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
|
|
46
|
+
}
|
|
47
|
+
// ── Technical Analysis Helpers ──
|
|
48
|
+
function sma(data, period) {
|
|
49
|
+
const result = [];
|
|
50
|
+
for (let i = period - 1; i < data.length; i++) {
|
|
51
|
+
const slice = data.slice(i - period + 1, i + 1);
|
|
52
|
+
result.push(slice.reduce((a, b) => a + b, 0) / period);
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
function ema(data, period) {
|
|
57
|
+
const k = 2 / (period + 1);
|
|
58
|
+
const result = [data[0]];
|
|
59
|
+
for (let i = 1; i < data.length; i++) {
|
|
60
|
+
result.push(data[i] * k + result[i - 1] * (1 - k));
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
function rsi(closes, period = 14) {
|
|
65
|
+
const gains = [];
|
|
66
|
+
const losses = [];
|
|
67
|
+
for (let i = 1; i < closes.length; i++) {
|
|
68
|
+
const diff = closes[i] - closes[i - 1];
|
|
69
|
+
gains.push(diff > 0 ? diff : 0);
|
|
70
|
+
losses.push(diff < 0 ? -diff : 0);
|
|
71
|
+
}
|
|
72
|
+
const result = [];
|
|
73
|
+
let avgGain = gains.slice(0, period).reduce((a, b) => a + b, 0) / period;
|
|
74
|
+
let avgLoss = losses.slice(0, period).reduce((a, b) => a + b, 0) / period;
|
|
75
|
+
for (let i = period; i < gains.length; i++) {
|
|
76
|
+
avgGain = (avgGain * (period - 1) + gains[i]) / period;
|
|
77
|
+
avgLoss = (avgLoss * (period - 1) + losses[i]) / period;
|
|
78
|
+
const rs = avgLoss === 0 ? 100 : avgGain / avgLoss;
|
|
79
|
+
result.push(100 - 100 / (1 + rs));
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
function bollingerBands(closes, period = 20, stdDevMult = 2) {
|
|
84
|
+
const middle = sma(closes, period);
|
|
85
|
+
const upper = [];
|
|
86
|
+
const lower = [];
|
|
87
|
+
for (let i = 0; i < middle.length; i++) {
|
|
88
|
+
const slice = closes.slice(i, i + period);
|
|
89
|
+
const mean = middle[i];
|
|
90
|
+
const variance = slice.reduce((sum, v) => sum + (v - mean) ** 2, 0) / period;
|
|
91
|
+
const stdDev = Math.sqrt(variance);
|
|
92
|
+
upper.push(mean + stdDevMult * stdDev);
|
|
93
|
+
lower.push(mean - stdDevMult * stdDev);
|
|
94
|
+
}
|
|
95
|
+
return { upper, middle, lower };
|
|
96
|
+
}
|
|
97
|
+
// ── Register Tools ──
|
|
98
|
+
export function registerFinanceTools() {
|
|
99
|
+
// ─── Market Data ───
|
|
100
|
+
registerTool({
|
|
101
|
+
name: 'market_data',
|
|
102
|
+
description: 'Get current price, market cap, volume, and 24h change for any cryptocurrency or token. Uses CoinGecko (free, no API key). Supports 10,000+ tokens.',
|
|
103
|
+
parameters: {
|
|
104
|
+
symbol: { type: 'string', description: 'Token symbol or CoinGecko ID (e.g. "bitcoin", "ethereum", "solana", "BTC", "ETH")', required: true },
|
|
105
|
+
currency: { type: 'string', description: 'Fiat currency for prices (default: usd)', default: 'usd' },
|
|
106
|
+
},
|
|
107
|
+
tier: 'free',
|
|
108
|
+
timeout: 15_000,
|
|
109
|
+
async execute(args) {
|
|
110
|
+
const symbol = String(args.symbol).toLowerCase();
|
|
111
|
+
const currency = String(args.currency || 'usd').toLowerCase();
|
|
112
|
+
// Map common ticker symbols to CoinGecko IDs
|
|
113
|
+
const symbolMap = {
|
|
114
|
+
btc: 'bitcoin', eth: 'ethereum', sol: 'solana', bnb: 'binancecoin',
|
|
115
|
+
ada: 'cardano', dot: 'polkadot', avax: 'avalanche-2', matic: 'matic-network',
|
|
116
|
+
link: 'chainlink', uni: 'uniswap', aave: 'aave', doge: 'dogecoin',
|
|
117
|
+
shib: 'shiba-inu', xrp: 'ripple', ltc: 'litecoin', atom: 'cosmos',
|
|
118
|
+
near: 'near', apt: 'aptos', arb: 'arbitrum', op: 'optimism',
|
|
119
|
+
sui: 'sui', sei: 'sei-network', jup: 'jupiter-exchange-solana',
|
|
120
|
+
};
|
|
121
|
+
const id = symbolMap[symbol] || symbol;
|
|
122
|
+
const data = await fetchJSON(`https://api.coingecko.com/api/v3/coins/${id}?localization=false&tickers=false&community_data=false&developer_data=false`);
|
|
123
|
+
const market = data.market_data;
|
|
124
|
+
if (!market)
|
|
125
|
+
return `Could not find data for "${symbol}". Try the full CoinGecko ID (e.g. "bitcoin" not "btc").`;
|
|
126
|
+
const price = market.current_price?.[currency] ?? 0;
|
|
127
|
+
const change24h = market.price_change_percentage_24h ?? 0;
|
|
128
|
+
const change7d = market.price_change_percentage_7d ?? 0;
|
|
129
|
+
const change30d = market.price_change_percentage_30d ?? 0;
|
|
130
|
+
const marketCap = market.market_cap?.[currency] ?? 0;
|
|
131
|
+
const volume = market.total_volume?.[currency] ?? 0;
|
|
132
|
+
const high24h = market.high_24h?.[currency] ?? 0;
|
|
133
|
+
const low24h = market.low_24h?.[currency] ?? 0;
|
|
134
|
+
const ath = market.ath?.[currency] ?? 0;
|
|
135
|
+
const athDate = market.ath_date?.[currency] ?? '';
|
|
136
|
+
const athChange = market.ath_change_percentage?.[currency] ?? 0;
|
|
137
|
+
return [
|
|
138
|
+
`## ${data.name} (${data.symbol.toUpperCase()})`,
|
|
139
|
+
'',
|
|
140
|
+
`**Price**: $${fmt(price, price < 1 ? 6 : 2)}`,
|
|
141
|
+
`**24h**: ${change24h >= 0 ? '+' : ''}${fmt(change24h)}% | **7d**: ${change7d >= 0 ? '+' : ''}${fmt(change7d)}% | **30d**: ${change30d >= 0 ? '+' : ''}${fmt(change30d)}%`,
|
|
142
|
+
`**24h Range**: $${fmt(low24h, price < 1 ? 6 : 2)} — $${fmt(high24h, price < 1 ? 6 : 2)}`,
|
|
143
|
+
`**Market Cap**: $${fmt(marketCap, 0)}`,
|
|
144
|
+
`**24h Volume**: $${fmt(volume, 0)}`,
|
|
145
|
+
`**ATH**: $${fmt(ath, price < 1 ? 6 : 2)} (${athDate.split('T')[0]}) — ${fmt(athChange)}% from ATH`,
|
|
146
|
+
'',
|
|
147
|
+
`*Data from CoinGecko — ${new Date().toISOString().split('T')[0]}*`,
|
|
148
|
+
].join('\n');
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
registerTool({
|
|
152
|
+
name: 'market_overview',
|
|
153
|
+
description: 'Get a snapshot of the entire crypto market — total market cap, BTC dominance, top gainers/losers, trending coins. Good for daily briefings.',
|
|
154
|
+
parameters: {
|
|
155
|
+
limit: { type: 'number', description: 'Number of top coins to show (default: 10, max: 50)', default: 10 },
|
|
156
|
+
},
|
|
157
|
+
tier: 'free',
|
|
158
|
+
timeout: 15_000,
|
|
159
|
+
async execute(args) {
|
|
160
|
+
const limit = Math.min(Number(args.limit) || 10, 50);
|
|
161
|
+
const [globalData, marketsData] = await Promise.all([
|
|
162
|
+
fetchJSON('https://api.coingecko.com/api/v3/global'),
|
|
163
|
+
fetchJSON(`https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=${limit}&page=1&sparkline=false&price_change_percentage=24h,7d`),
|
|
164
|
+
]);
|
|
165
|
+
const g = globalData.data;
|
|
166
|
+
const lines = [
|
|
167
|
+
'## Crypto Market Overview',
|
|
168
|
+
'',
|
|
169
|
+
`**Total Market Cap**: $${fmt(g.total_market_cap?.usd ?? 0, 0)}`,
|
|
170
|
+
`**24h Volume**: $${fmt(g.total_volume?.usd ?? 0, 0)}`,
|
|
171
|
+
`**BTC Dominance**: ${fmt(g.market_cap_percentage?.btc ?? 0)}%`,
|
|
172
|
+
`**ETH Dominance**: ${fmt(g.market_cap_percentage?.eth ?? 0)}%`,
|
|
173
|
+
`**Active Coins**: ${g.active_cryptocurrencies?.toLocaleString() ?? '?'}`,
|
|
174
|
+
'',
|
|
175
|
+
`### Top ${limit} by Market Cap`,
|
|
176
|
+
'',
|
|
177
|
+
'| # | Coin | Price | 24h | 7d | Market Cap |',
|
|
178
|
+
'|---|------|-------|-----|-----|------------|',
|
|
179
|
+
];
|
|
180
|
+
for (let i = 0; i < marketsData.length; i++) {
|
|
181
|
+
const c = marketsData[i];
|
|
182
|
+
const p = c.current_price;
|
|
183
|
+
const ch24 = c.price_change_percentage_24h_in_currency ?? 0;
|
|
184
|
+
const ch7d = c.price_change_percentage_7d_in_currency ?? 0;
|
|
185
|
+
lines.push(`| ${i + 1} | ${c.symbol.toUpperCase()} | $${fmt(p, p < 1 ? 4 : 2)} | ${ch24 >= 0 ? '+' : ''}${fmt(ch24)}% | ${ch7d >= 0 ? '+' : ''}${fmt(ch7d)}% | $${fmt(c.market_cap, 0)} |`);
|
|
186
|
+
}
|
|
187
|
+
lines.push('', `*${new Date().toISOString().split('T')[0]}*`);
|
|
188
|
+
return lines.join('\n');
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
// ─── Price History & Charts ───
|
|
192
|
+
registerTool({
|
|
193
|
+
name: 'price_history',
|
|
194
|
+
description: 'Get historical price data (OHLCV) for a cryptocurrency. Returns daily candles. Useful for charting, backtesting, and trend analysis.',
|
|
195
|
+
parameters: {
|
|
196
|
+
symbol: { type: 'string', description: 'Token symbol or CoinGecko ID', required: true },
|
|
197
|
+
days: { type: 'number', description: 'Number of days of history (1, 7, 14, 30, 90, 180, 365, max)', default: 30 },
|
|
198
|
+
},
|
|
199
|
+
tier: 'free',
|
|
200
|
+
timeout: 15_000,
|
|
201
|
+
async execute(args) {
|
|
202
|
+
const symbol = String(args.symbol).toLowerCase();
|
|
203
|
+
const days = Number(args.days) || 30;
|
|
204
|
+
const symbolMap = {
|
|
205
|
+
btc: 'bitcoin', eth: 'ethereum', sol: 'solana', bnb: 'binancecoin',
|
|
206
|
+
ada: 'cardano', dot: 'polkadot', doge: 'dogecoin', xrp: 'ripple',
|
|
207
|
+
};
|
|
208
|
+
const id = symbolMap[symbol] || symbol;
|
|
209
|
+
const data = await fetchJSON(`https://api.coingecko.com/api/v3/coins/${id}/ohlc?vs_currency=usd&days=${days}`);
|
|
210
|
+
if (!data?.length)
|
|
211
|
+
return `No price history for "${symbol}".`;
|
|
212
|
+
const lines = [
|
|
213
|
+
`## ${id.toUpperCase()} — ${days}d OHLCV`,
|
|
214
|
+
'',
|
|
215
|
+
'| Date | Open | High | Low | Close |',
|
|
216
|
+
'|------|------|------|-----|-------|',
|
|
217
|
+
];
|
|
218
|
+
// Show at most 30 rows (sample if more data)
|
|
219
|
+
const step = Math.max(1, Math.floor(data.length / 30));
|
|
220
|
+
for (let i = 0; i < data.length; i += step) {
|
|
221
|
+
const [ts, o, h, l, c] = data[i];
|
|
222
|
+
const date = new Date(ts).toISOString().split('T')[0];
|
|
223
|
+
lines.push(`| ${date} | $${fmt(o)} | $${fmt(h)} | $${fmt(l)} | $${fmt(c)} |`);
|
|
224
|
+
}
|
|
225
|
+
// Summary stats
|
|
226
|
+
const closes = data.map(d => d[4]);
|
|
227
|
+
const first = closes[0];
|
|
228
|
+
const last = closes[closes.length - 1];
|
|
229
|
+
const changePct = ((last - first) / first) * 100;
|
|
230
|
+
const high = Math.max(...data.map(d => d[2]));
|
|
231
|
+
const low = Math.min(...data.map(d => d[3]));
|
|
232
|
+
lines.push('', `**Period Return**: ${changePct >= 0 ? '+' : ''}${fmt(changePct)}%`, `**Period High**: $${fmt(high)} | **Low**: $${fmt(low)}`, `**Volatility**: $${fmt(high - low)} range (${fmt(((high - low) / last) * 100)}% of current price)`);
|
|
233
|
+
return lines.join('\n');
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
// ─── Technical Analysis ───
|
|
237
|
+
registerTool({
|
|
238
|
+
name: 'technical_analysis',
|
|
239
|
+
description: 'Run technical analysis on a cryptocurrency — RSI, moving averages (SMA/EMA), Bollinger Bands, MACD signal. Returns actionable signals.',
|
|
240
|
+
parameters: {
|
|
241
|
+
symbol: { type: 'string', description: 'Token symbol or CoinGecko ID', required: true },
|
|
242
|
+
days: { type: 'number', description: 'Days of data to analyze (default: 90)', default: 90 },
|
|
243
|
+
},
|
|
244
|
+
tier: 'free',
|
|
245
|
+
timeout: 15_000,
|
|
246
|
+
async execute(args) {
|
|
247
|
+
const symbol = String(args.symbol).toLowerCase();
|
|
248
|
+
const days = Number(args.days) || 90;
|
|
249
|
+
const symbolMap = {
|
|
250
|
+
btc: 'bitcoin', eth: 'ethereum', sol: 'solana', bnb: 'binancecoin',
|
|
251
|
+
ada: 'cardano', dot: 'polkadot', doge: 'dogecoin', xrp: 'ripple',
|
|
252
|
+
};
|
|
253
|
+
const id = symbolMap[symbol] || symbol;
|
|
254
|
+
const data = await fetchJSON(`https://api.coingecko.com/api/v3/coins/${id}/ohlc?vs_currency=usd&days=${days}`);
|
|
255
|
+
if (!data?.length || data.length < 30)
|
|
256
|
+
return `Not enough data for technical analysis on "${symbol}". Need at least 30 days.`;
|
|
257
|
+
const closes = data.map(d => d[4]);
|
|
258
|
+
const current = closes[closes.length - 1];
|
|
259
|
+
// RSI
|
|
260
|
+
const rsiValues = rsi(closes, 14);
|
|
261
|
+
const currentRsi = rsiValues[rsiValues.length - 1];
|
|
262
|
+
const rsiSignal = currentRsi > 70 ? 'OVERBOUGHT' : currentRsi < 30 ? 'OVERSOLD' : 'NEUTRAL';
|
|
263
|
+
// Moving averages
|
|
264
|
+
const sma20 = sma(closes, 20);
|
|
265
|
+
const sma50 = sma(closes, Math.min(50, Math.floor(closes.length / 2)));
|
|
266
|
+
const ema12 = ema(closes, 12);
|
|
267
|
+
const ema26 = ema(closes, 26);
|
|
268
|
+
const currentSma20 = sma20[sma20.length - 1];
|
|
269
|
+
const currentSma50 = sma50[sma50.length - 1];
|
|
270
|
+
// MACD
|
|
271
|
+
const macdLine = ema12[ema12.length - 1] - ema26[ema26.length - 1];
|
|
272
|
+
const macdSignal = macdLine > 0 ? 'BULLISH' : 'BEARISH';
|
|
273
|
+
// Bollinger Bands
|
|
274
|
+
const bb = bollingerBands(closes, 20);
|
|
275
|
+
const bbUpper = bb.upper[bb.upper.length - 1];
|
|
276
|
+
const bbLower = bb.lower[bb.lower.length - 1];
|
|
277
|
+
const bbMiddle = bb.middle[bb.middle.length - 1];
|
|
278
|
+
const bbPosition = current > bbUpper ? 'ABOVE UPPER BAND' : current < bbLower ? 'BELOW LOWER BAND' : 'WITHIN BANDS';
|
|
279
|
+
// Trend
|
|
280
|
+
const trend = current > currentSma20 && currentSma20 > currentSma50 ? 'UPTREND'
|
|
281
|
+
: current < currentSma20 && currentSma20 < currentSma50 ? 'DOWNTREND'
|
|
282
|
+
: 'SIDEWAYS';
|
|
283
|
+
// Overall signal
|
|
284
|
+
let bullish = 0;
|
|
285
|
+
let bearish = 0;
|
|
286
|
+
if (currentRsi < 30)
|
|
287
|
+
bullish++;
|
|
288
|
+
if (currentRsi > 70)
|
|
289
|
+
bearish++;
|
|
290
|
+
if (current > currentSma20)
|
|
291
|
+
bullish++;
|
|
292
|
+
else
|
|
293
|
+
bearish++;
|
|
294
|
+
if (current > currentSma50)
|
|
295
|
+
bullish++;
|
|
296
|
+
else
|
|
297
|
+
bearish++;
|
|
298
|
+
if (macdLine > 0)
|
|
299
|
+
bullish++;
|
|
300
|
+
else
|
|
301
|
+
bearish++;
|
|
302
|
+
if (current < bbLower)
|
|
303
|
+
bullish++;
|
|
304
|
+
if (current > bbUpper)
|
|
305
|
+
bearish++;
|
|
306
|
+
const overall = bullish > bearish ? 'BULLISH' : bearish > bullish ? 'BEARISH' : 'NEUTRAL';
|
|
307
|
+
return [
|
|
308
|
+
`## ${id.toUpperCase()} — Technical Analysis`,
|
|
309
|
+
'',
|
|
310
|
+
`**Price**: $${fmt(current)} | **Trend**: ${trend}`,
|
|
311
|
+
'',
|
|
312
|
+
'### Indicators',
|
|
313
|
+
'',
|
|
314
|
+
`| Indicator | Value | Signal |`,
|
|
315
|
+
`|-----------|-------|--------|`,
|
|
316
|
+
`| RSI (14) | ${fmt(currentRsi)} | ${rsiSignal} |`,
|
|
317
|
+
`| SMA (20) | $${fmt(currentSma20)} | Price ${current > currentSma20 ? 'above' : 'below'} |`,
|
|
318
|
+
`| SMA (50) | $${fmt(currentSma50)} | Price ${current > currentSma50 ? 'above' : 'below'} |`,
|
|
319
|
+
`| MACD | ${fmt(macdLine, 4)} | ${macdSignal} |`,
|
|
320
|
+
`| Bollinger | $${fmt(bbLower)} — $${fmt(bbUpper)} | ${bbPosition} |`,
|
|
321
|
+
'',
|
|
322
|
+
`### Signal Summary: **${overall}** (${bullish} bullish / ${bearish} bearish)`,
|
|
323
|
+
'',
|
|
324
|
+
`*${days}d analysis — ${new Date().toISOString().split('T')[0]}. Not financial advice.*`,
|
|
325
|
+
].join('\n');
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
// ─── Paper Trading ───
|
|
329
|
+
registerTool({
|
|
330
|
+
name: 'paper_trade',
|
|
331
|
+
description: 'Execute a simulated trade in your paper portfolio. Starts with $100,000 virtual cash. Use this to test strategies risk-free. Enforces position limits and stop losses.',
|
|
332
|
+
parameters: {
|
|
333
|
+
action: { type: 'string', description: 'Action: "buy", "sell", "portfolio", "reset", "history"', required: true },
|
|
334
|
+
symbol: { type: 'string', description: 'Token symbol (required for buy/sell)' },
|
|
335
|
+
amount: { type: 'number', description: 'USD amount to buy, or quantity to sell' },
|
|
336
|
+
},
|
|
337
|
+
tier: 'free',
|
|
338
|
+
async execute(args) {
|
|
339
|
+
const action = String(args.action).toLowerCase();
|
|
340
|
+
const portfolio = loadPortfolio();
|
|
341
|
+
if (action === 'reset') {
|
|
342
|
+
const fresh = {
|
|
343
|
+
cash: 100_000,
|
|
344
|
+
positions: [],
|
|
345
|
+
trades: [],
|
|
346
|
+
createdAt: new Date().toISOString(),
|
|
347
|
+
limits: portfolio.limits,
|
|
348
|
+
};
|
|
349
|
+
savePortfolio(fresh);
|
|
350
|
+
return '**Paper portfolio reset.** Starting fresh with $100,000.';
|
|
351
|
+
}
|
|
352
|
+
if (action === 'history') {
|
|
353
|
+
if (!portfolio.trades.length)
|
|
354
|
+
return 'No trades yet. Use `paper_trade buy <symbol> <amount>` to start.';
|
|
355
|
+
const lines = ['## Trade History', '', '| Time | Action | Symbol | Qty | Price | P&L |', '|------|--------|--------|-----|-------|-----|'];
|
|
356
|
+
for (const t of portfolio.trades.slice(-20)) {
|
|
357
|
+
lines.push(`| ${t.timestamp.split('T')[0]} | ${t.side.toUpperCase()} | ${t.symbol} | ${fmt(t.quantity, 4)} | $${fmt(t.price)} | ${t.pnl != null ? `$${fmt(t.pnl)}` : '—'} |`);
|
|
358
|
+
}
|
|
359
|
+
return lines.join('\n');
|
|
360
|
+
}
|
|
361
|
+
if (action === 'portfolio') {
|
|
362
|
+
const lines = ['## Paper Portfolio', ''];
|
|
363
|
+
lines.push(`**Cash**: $${fmt(portfolio.cash)}`);
|
|
364
|
+
if (!portfolio.positions.length) {
|
|
365
|
+
lines.push('**Positions**: None');
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
lines.push('', '| Symbol | Qty | Avg Cost | Side |', '|--------|-----|----------|------|');
|
|
369
|
+
for (const p of portfolio.positions) {
|
|
370
|
+
lines.push(`| ${p.symbol} | ${fmt(p.quantity, 4)} | $${fmt(p.avgCost)} | ${p.side} |`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const totalPnl = portfolio.trades.reduce((sum, t) => sum + (t.pnl || 0), 0);
|
|
374
|
+
lines.push('', `**Realized P&L**: $${fmt(totalPnl)}`, `**Trades**: ${portfolio.trades.length}`);
|
|
375
|
+
return lines.join('\n');
|
|
376
|
+
}
|
|
377
|
+
if (action === 'buy' || action === 'sell') {
|
|
378
|
+
const symbol = String(args.symbol || '').toLowerCase();
|
|
379
|
+
if (!symbol)
|
|
380
|
+
return 'Error: symbol is required for buy/sell.';
|
|
381
|
+
const amount = Number(args.amount);
|
|
382
|
+
if (!amount || amount <= 0)
|
|
383
|
+
return 'Error: amount must be a positive number.';
|
|
384
|
+
// Fetch current price
|
|
385
|
+
const symbolMap = {
|
|
386
|
+
btc: 'bitcoin', eth: 'ethereum', sol: 'solana', bnb: 'binancecoin',
|
|
387
|
+
ada: 'cardano', doge: 'dogecoin', xrp: 'ripple',
|
|
388
|
+
};
|
|
389
|
+
const id = symbolMap[symbol] || symbol;
|
|
390
|
+
const priceData = await fetchJSON(`https://api.coingecko.com/api/v3/simple/price?ids=${id}&vs_currencies=usd`);
|
|
391
|
+
const price = priceData[id]?.usd;
|
|
392
|
+
if (!price)
|
|
393
|
+
return `Could not fetch price for "${symbol}".`;
|
|
394
|
+
if (action === 'buy') {
|
|
395
|
+
if (amount > portfolio.cash)
|
|
396
|
+
return `Insufficient cash. Have $${fmt(portfolio.cash)}, need $${fmt(amount)}.`;
|
|
397
|
+
// Check position limit
|
|
398
|
+
const totalValue = portfolio.cash + portfolio.positions.reduce((s, p) => s + p.quantity * p.avgCost, 0);
|
|
399
|
+
if (amount / totalValue * 100 > portfolio.limits.maxPositionPct) {
|
|
400
|
+
return `**RISK LIMIT**: Position would exceed ${portfolio.limits.maxPositionPct}% of portfolio. Max buy: $${fmt(totalValue * portfolio.limits.maxPositionPct / 100)}.`;
|
|
401
|
+
}
|
|
402
|
+
const qty = amount / price;
|
|
403
|
+
const existing = portfolio.positions.find(p => p.symbol === symbol && p.side === 'long');
|
|
404
|
+
if (existing) {
|
|
405
|
+
const totalQty = existing.quantity + qty;
|
|
406
|
+
existing.avgCost = (existing.avgCost * existing.quantity + price * qty) / totalQty;
|
|
407
|
+
existing.quantity = totalQty;
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
portfolio.positions.push({ symbol, quantity: qty, avgCost: price, side: 'long', openedAt: new Date().toISOString() });
|
|
411
|
+
}
|
|
412
|
+
portfolio.cash -= amount;
|
|
413
|
+
portfolio.trades.push({ symbol, side: 'buy', quantity: qty, price, timestamp: new Date().toISOString() });
|
|
414
|
+
savePortfolio(portfolio);
|
|
415
|
+
return `**BOUGHT** ${fmt(qty, 6)} ${symbol.toUpperCase()} @ $${fmt(price)} for $${fmt(amount)}.\nCash remaining: $${fmt(portfolio.cash)}`;
|
|
416
|
+
}
|
|
417
|
+
if (action === 'sell') {
|
|
418
|
+
const pos = portfolio.positions.find(p => p.symbol === symbol && p.side === 'long');
|
|
419
|
+
if (!pos)
|
|
420
|
+
return `No ${symbol.toUpperCase()} position to sell.`;
|
|
421
|
+
const qty = Math.min(amount, pos.quantity);
|
|
422
|
+
const proceeds = qty * price;
|
|
423
|
+
const costBasis = qty * pos.avgCost;
|
|
424
|
+
const pnl = proceeds - costBasis;
|
|
425
|
+
pos.quantity -= qty;
|
|
426
|
+
if (pos.quantity < 0.000001) {
|
|
427
|
+
portfolio.positions = portfolio.positions.filter(p => p !== pos);
|
|
428
|
+
}
|
|
429
|
+
portfolio.cash += proceeds;
|
|
430
|
+
portfolio.trades.push({ symbol, side: 'sell', quantity: qty, price, timestamp: new Date().toISOString(), pnl });
|
|
431
|
+
savePortfolio(portfolio);
|
|
432
|
+
return `**SOLD** ${fmt(qty, 6)} ${symbol.toUpperCase()} @ $${fmt(price)} for $${fmt(proceeds)}.\nP&L: ${pnl >= 0 ? '+' : ''}$${fmt(pnl)}\nCash: $${fmt(portfolio.cash)}`;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return 'Unknown action. Use: buy, sell, portfolio, history, or reset.';
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
// ─── Wallet Balance (Read-Only) ───
|
|
439
|
+
registerTool({
|
|
440
|
+
name: 'wallet_balance',
|
|
441
|
+
description: 'Check the balance of any crypto wallet address (Solana or Ethereum). Read-only — does not require private keys. Uses public RPC endpoints.',
|
|
442
|
+
parameters: {
|
|
443
|
+
address: { type: 'string', description: 'Wallet address (Solana or Ethereum)', required: true },
|
|
444
|
+
chain: { type: 'string', description: 'Blockchain: "solana" or "ethereum" (auto-detected if omitted)' },
|
|
445
|
+
},
|
|
446
|
+
tier: 'free',
|
|
447
|
+
timeout: 15_000,
|
|
448
|
+
async execute(args) {
|
|
449
|
+
const address = String(args.address).trim();
|
|
450
|
+
let chain = String(args.chain || '').toLowerCase();
|
|
451
|
+
// Auto-detect chain from address format
|
|
452
|
+
if (!chain) {
|
|
453
|
+
if (address.startsWith('0x') && address.length === 42)
|
|
454
|
+
chain = 'ethereum';
|
|
455
|
+
else if (address.length >= 32 && address.length <= 44 && !address.startsWith('0x'))
|
|
456
|
+
chain = 'solana';
|
|
457
|
+
else
|
|
458
|
+
return 'Could not detect chain. Specify chain: "solana" or "ethereum".';
|
|
459
|
+
}
|
|
460
|
+
if (chain === 'solana') {
|
|
461
|
+
const body = JSON.stringify({
|
|
462
|
+
jsonrpc: '2.0', id: 1, method: 'getBalance', params: [address],
|
|
463
|
+
});
|
|
464
|
+
const res = await fetch('https://api.mainnet-beta.solana.com', {
|
|
465
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' }, body,
|
|
466
|
+
signal: AbortSignal.timeout(10_000),
|
|
467
|
+
});
|
|
468
|
+
const data = await res.json();
|
|
469
|
+
if (data.error)
|
|
470
|
+
return `Error: ${data.error.message}`;
|
|
471
|
+
const lamports = data.result?.value ?? 0;
|
|
472
|
+
const sol = lamports / 1e9;
|
|
473
|
+
// Get SOL price
|
|
474
|
+
const priceData = await fetchJSON('https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd');
|
|
475
|
+
const solPrice = priceData.solana?.usd ?? 0;
|
|
476
|
+
return [
|
|
477
|
+
`## Solana Wallet`,
|
|
478
|
+
`**Address**: \`${address.slice(0, 6)}...${address.slice(-4)}\``,
|
|
479
|
+
`**Balance**: ${fmt(sol, 4)} SOL ($${fmt(sol * solPrice)})`,
|
|
480
|
+
`**SOL Price**: $${fmt(solPrice)}`,
|
|
481
|
+
].join('\n');
|
|
482
|
+
}
|
|
483
|
+
if (chain === 'ethereum') {
|
|
484
|
+
const body = JSON.stringify({
|
|
485
|
+
jsonrpc: '2.0', id: 1, method: 'eth_getBalance', params: [address, 'latest'],
|
|
486
|
+
});
|
|
487
|
+
const res = await fetch('https://eth.llamarpc.com', {
|
|
488
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' }, body,
|
|
489
|
+
signal: AbortSignal.timeout(10_000),
|
|
490
|
+
});
|
|
491
|
+
const data = await res.json();
|
|
492
|
+
if (data.error)
|
|
493
|
+
return `Error: ${data.error.message}`;
|
|
494
|
+
const wei = parseInt(data.result, 16);
|
|
495
|
+
const eth = wei / 1e18;
|
|
496
|
+
const priceData = await fetchJSON('https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd');
|
|
497
|
+
const ethPrice = priceData.ethereum?.usd ?? 0;
|
|
498
|
+
return [
|
|
499
|
+
`## Ethereum Wallet`,
|
|
500
|
+
`**Address**: \`${address.slice(0, 6)}...${address.slice(-4)}\``,
|
|
501
|
+
`**Balance**: ${fmt(eth, 6)} ETH ($${fmt(eth * ethPrice)})`,
|
|
502
|
+
`**ETH Price**: $${fmt(ethPrice)}`,
|
|
503
|
+
].join('\n');
|
|
504
|
+
}
|
|
505
|
+
return `Unsupported chain: ${chain}. Use "solana" or "ethereum".`;
|
|
506
|
+
},
|
|
507
|
+
});
|
|
508
|
+
// ─── Market Sentiment ───
|
|
509
|
+
registerTool({
|
|
510
|
+
name: 'market_sentiment',
|
|
511
|
+
description: 'Get crypto market sentiment — Fear & Greed Index, trending coins, and social signals. Useful for gauging market mood before trading.',
|
|
512
|
+
parameters: {},
|
|
513
|
+
tier: 'free',
|
|
514
|
+
timeout: 15_000,
|
|
515
|
+
async execute() {
|
|
516
|
+
const lines = ['## Market Sentiment'];
|
|
517
|
+
// Fear & Greed Index
|
|
518
|
+
try {
|
|
519
|
+
const fg = await fetchJSON('https://api.alternative.me/fng/?limit=7');
|
|
520
|
+
if (fg.data?.length) {
|
|
521
|
+
const latest = fg.data[0];
|
|
522
|
+
const emoji = Number(latest.value) <= 25 ? '😱' : Number(latest.value) <= 45 ? '😟' : Number(latest.value) <= 55 ? '😐' : Number(latest.value) <= 75 ? '😊' : '🤑';
|
|
523
|
+
lines.push('', `### Fear & Greed Index: ${latest.value}/100 ${emoji} (${latest.value_classification})`, '', '| Date | Value | Classification |', '|------|-------|----------------|');
|
|
524
|
+
for (const d of fg.data) {
|
|
525
|
+
lines.push(`| ${new Date(Number(d.timestamp) * 1000).toISOString().split('T')[0]} | ${d.value} | ${d.value_classification} |`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
catch {
|
|
530
|
+
lines.push('', '*Fear & Greed Index unavailable*');
|
|
531
|
+
}
|
|
532
|
+
// Trending on CoinGecko
|
|
533
|
+
try {
|
|
534
|
+
const trending = await fetchJSON('https://api.coingecko.com/api/v3/search/trending');
|
|
535
|
+
if (trending.coins?.length) {
|
|
536
|
+
lines.push('', '### Trending Coins (CoinGecko)', '');
|
|
537
|
+
for (const c of trending.coins.slice(0, 7)) {
|
|
538
|
+
const item = c.item;
|
|
539
|
+
lines.push(`- **${item.name}** (${item.symbol}) — #${item.market_cap_rank || '?'} market cap`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
catch { /* skip */ }
|
|
544
|
+
lines.push('', `*${new Date().toISOString().split('T')[0]}*`);
|
|
545
|
+
return lines.join('\n');
|
|
546
|
+
},
|
|
547
|
+
});
|
|
548
|
+
// ─── DeFi Yields ───
|
|
549
|
+
registerTool({
|
|
550
|
+
name: 'defi_yields',
|
|
551
|
+
description: 'Show top DeFi yield opportunities across protocols (Aave, Compound, Lido, etc). Useful for finding passive income strategies.',
|
|
552
|
+
parameters: {
|
|
553
|
+
chain: { type: 'string', description: 'Filter by chain: ethereum, solana, arbitrum, etc. (default: all)' },
|
|
554
|
+
limit: { type: 'number', description: 'Number of results (default: 15)', default: 15 },
|
|
555
|
+
},
|
|
556
|
+
tier: 'free',
|
|
557
|
+
timeout: 15_000,
|
|
558
|
+
async execute(args) {
|
|
559
|
+
const chain = args.chain ? String(args.chain).toLowerCase() : undefined;
|
|
560
|
+
const limit = Math.min(Number(args.limit) || 15, 30);
|
|
561
|
+
const data = await fetchJSON('https://yields.llama.fi/pools');
|
|
562
|
+
if (!data?.data?.length)
|
|
563
|
+
return 'Could not fetch DeFi yield data.';
|
|
564
|
+
let pools = data.data
|
|
565
|
+
.filter((p) => p.tvlUsd > 1_000_000 && p.apy > 0.1) // >$1M TVL, >0.1% APY
|
|
566
|
+
.sort((a, b) => b.apy - a.apy);
|
|
567
|
+
if (chain)
|
|
568
|
+
pools = pools.filter((p) => p.chain?.toLowerCase() === chain);
|
|
569
|
+
pools = pools.slice(0, limit);
|
|
570
|
+
const lines = [
|
|
571
|
+
`## Top DeFi Yields${chain ? ` (${chain})` : ''}`,
|
|
572
|
+
'',
|
|
573
|
+
'| Protocol | Pool | Chain | APY | TVL |',
|
|
574
|
+
'|----------|------|-------|-----|-----|',
|
|
575
|
+
];
|
|
576
|
+
for (const p of pools) {
|
|
577
|
+
lines.push(`| ${p.project} | ${p.symbol} | ${p.chain} | ${fmt(p.apy)}% | $${fmt(p.tvlUsd, 0)} |`);
|
|
578
|
+
}
|
|
579
|
+
lines.push('', '*Data from DeFiLlama. APY fluctuates. DYOR.*');
|
|
580
|
+
return lines.join('\n');
|
|
581
|
+
},
|
|
582
|
+
});
|
|
583
|
+
// ─── Backtesting Engine ───
|
|
584
|
+
registerTool({
|
|
585
|
+
name: 'backtest_strategy',
|
|
586
|
+
description: 'Backtest a trading strategy against historical crypto data. Supports: DCA (dollar-cost averaging), momentum (buy on RSI oversold, sell overbought), and mean-reversion (buy below lower Bollinger, sell above upper). Returns P&L, win rate, max drawdown.',
|
|
587
|
+
parameters: {
|
|
588
|
+
symbol: { type: 'string', description: 'Token symbol or CoinGecko ID', required: true },
|
|
589
|
+
strategy: { type: 'string', description: 'Strategy: "dca", "momentum", or "mean-reversion"', required: true },
|
|
590
|
+
days: { type: 'number', description: 'Days of historical data to test against (default: 180)', default: 180 },
|
|
591
|
+
investment: { type: 'number', description: 'Total investment amount in USD (default: 10000)', default: 10000 },
|
|
592
|
+
},
|
|
593
|
+
tier: 'free',
|
|
594
|
+
timeout: 20_000,
|
|
595
|
+
async execute(args) {
|
|
596
|
+
const symbol = String(args.symbol).toLowerCase();
|
|
597
|
+
const strategy = String(args.strategy).toLowerCase();
|
|
598
|
+
const days = Number(args.days) || 180;
|
|
599
|
+
const investment = Number(args.investment) || 10000;
|
|
600
|
+
const symbolMap = {
|
|
601
|
+
btc: 'bitcoin', eth: 'ethereum', sol: 'solana', bnb: 'binancecoin',
|
|
602
|
+
ada: 'cardano', doge: 'dogecoin', xrp: 'ripple',
|
|
603
|
+
};
|
|
604
|
+
const id = symbolMap[symbol] || symbol;
|
|
605
|
+
const data = await fetchJSON(`https://api.coingecko.com/api/v3/coins/${id}/ohlc?vs_currency=usd&days=${days}`);
|
|
606
|
+
if (!data?.length || data.length < 30)
|
|
607
|
+
return `Not enough data for backtest. Need at least 30 days.`;
|
|
608
|
+
const closes = data.map(d => d[4]);
|
|
609
|
+
const trades = [];
|
|
610
|
+
let cash = investment;
|
|
611
|
+
let holdings = 0;
|
|
612
|
+
let peakValue = investment;
|
|
613
|
+
let maxDrawdown = 0;
|
|
614
|
+
if (strategy === 'dca') {
|
|
615
|
+
// Buy equal amounts at regular intervals
|
|
616
|
+
const intervals = Math.min(closes.length, 30); // up to 30 buys
|
|
617
|
+
const step = Math.floor(closes.length / intervals);
|
|
618
|
+
const perBuy = investment / intervals;
|
|
619
|
+
for (let i = 0; i < closes.length; i += step) {
|
|
620
|
+
if (cash < perBuy * 0.1)
|
|
621
|
+
break;
|
|
622
|
+
const buyAmt = Math.min(perBuy, cash);
|
|
623
|
+
const qty = buyAmt / closes[i];
|
|
624
|
+
holdings += qty;
|
|
625
|
+
cash -= buyAmt;
|
|
626
|
+
trades.push({ day: i, action: 'BUY', price: closes[i], qty });
|
|
627
|
+
const currentValue = cash + holdings * closes[i];
|
|
628
|
+
peakValue = Math.max(peakValue, currentValue);
|
|
629
|
+
const dd = (peakValue - currentValue) / peakValue * 100;
|
|
630
|
+
maxDrawdown = Math.max(maxDrawdown, dd);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
else if (strategy === 'momentum') {
|
|
634
|
+
// RSI momentum: buy when RSI < 30, sell when RSI > 70
|
|
635
|
+
const rsiValues = rsi(closes, 14);
|
|
636
|
+
const rsiOffset = closes.length - rsiValues.length;
|
|
637
|
+
for (let i = 0; i < rsiValues.length; i++) {
|
|
638
|
+
const priceIdx = i + rsiOffset;
|
|
639
|
+
if (rsiValues[i] < 30 && cash > 0) {
|
|
640
|
+
// Buy with 25% of remaining cash
|
|
641
|
+
const buyAmt = cash * 0.25;
|
|
642
|
+
const qty = buyAmt / closes[priceIdx];
|
|
643
|
+
holdings += qty;
|
|
644
|
+
cash -= buyAmt;
|
|
645
|
+
trades.push({ day: priceIdx, action: 'BUY', price: closes[priceIdx], qty });
|
|
646
|
+
}
|
|
647
|
+
else if (rsiValues[i] > 70 && holdings > 0) {
|
|
648
|
+
// Sell 50% of holdings
|
|
649
|
+
const sellQty = holdings * 0.5;
|
|
650
|
+
const proceeds = sellQty * closes[priceIdx];
|
|
651
|
+
holdings -= sellQty;
|
|
652
|
+
cash += proceeds;
|
|
653
|
+
trades.push({ day: priceIdx, action: 'SELL', price: closes[priceIdx], qty: sellQty });
|
|
654
|
+
}
|
|
655
|
+
const currentValue = cash + holdings * closes[priceIdx];
|
|
656
|
+
peakValue = Math.max(peakValue, currentValue);
|
|
657
|
+
const dd = (peakValue - currentValue) / peakValue * 100;
|
|
658
|
+
maxDrawdown = Math.max(maxDrawdown, dd);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
else if (strategy === 'mean-reversion') {
|
|
662
|
+
// Bollinger Bands: buy below lower band, sell above upper band
|
|
663
|
+
const bb = bollingerBands(closes, 20);
|
|
664
|
+
const bbOffset = closes.length - bb.middle.length;
|
|
665
|
+
for (let i = 0; i < bb.middle.length; i++) {
|
|
666
|
+
const priceIdx = i + bbOffset;
|
|
667
|
+
if (closes[priceIdx] < bb.lower[i] && cash > 0) {
|
|
668
|
+
const buyAmt = cash * 0.3;
|
|
669
|
+
const qty = buyAmt / closes[priceIdx];
|
|
670
|
+
holdings += qty;
|
|
671
|
+
cash -= buyAmt;
|
|
672
|
+
trades.push({ day: priceIdx, action: 'BUY', price: closes[priceIdx], qty });
|
|
673
|
+
}
|
|
674
|
+
else if (closes[priceIdx] > bb.upper[i] && holdings > 0) {
|
|
675
|
+
const sellQty = holdings * 0.5;
|
|
676
|
+
const proceeds = sellQty * closes[priceIdx];
|
|
677
|
+
holdings -= sellQty;
|
|
678
|
+
cash += proceeds;
|
|
679
|
+
trades.push({ day: priceIdx, action: 'SELL', price: closes[priceIdx], qty: sellQty });
|
|
680
|
+
}
|
|
681
|
+
const currentValue = cash + holdings * closes[priceIdx];
|
|
682
|
+
peakValue = Math.max(peakValue, currentValue);
|
|
683
|
+
const dd = (peakValue - currentValue) / peakValue * 100;
|
|
684
|
+
maxDrawdown = Math.max(maxDrawdown, dd);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
return `Unknown strategy "${strategy}". Use: dca, momentum, or mean-reversion.`;
|
|
689
|
+
}
|
|
690
|
+
// Final value
|
|
691
|
+
const finalPrice = closes[closes.length - 1];
|
|
692
|
+
const finalValue = cash + holdings * finalPrice;
|
|
693
|
+
const totalReturn = ((finalValue - investment) / investment) * 100;
|
|
694
|
+
const buyHoldReturn = ((finalPrice - closes[0]) / closes[0]) * 100;
|
|
695
|
+
// Win rate
|
|
696
|
+
const sellTrades = trades.filter(t => t.action === 'SELL');
|
|
697
|
+
const buyTrades = trades.filter(t => t.action === 'BUY');
|
|
698
|
+
const avgBuyPrice = buyTrades.length ? buyTrades.reduce((s, t) => s + t.price, 0) / buyTrades.length : 0;
|
|
699
|
+
const wins = sellTrades.filter(t => t.price > avgBuyPrice).length;
|
|
700
|
+
const winRate = sellTrades.length ? (wins / sellTrades.length) * 100 : 0;
|
|
701
|
+
return [
|
|
702
|
+
`## Backtest: ${strategy.toUpperCase()} on ${id.toUpperCase()}`,
|
|
703
|
+
'',
|
|
704
|
+
`**Period**: ${days} days | **Investment**: $${fmt(investment)}`,
|
|
705
|
+
'',
|
|
706
|
+
'### Results',
|
|
707
|
+
'',
|
|
708
|
+
`| Metric | Value |`,
|
|
709
|
+
`|--------|-------|`,
|
|
710
|
+
`| Final Value | $${fmt(finalValue)} |`,
|
|
711
|
+
`| Return | ${totalReturn >= 0 ? '+' : ''}${fmt(totalReturn)}% |`,
|
|
712
|
+
`| Buy & Hold Return | ${buyHoldReturn >= 0 ? '+' : ''}${fmt(buyHoldReturn)}% |`,
|
|
713
|
+
`| Alpha vs Buy & Hold | ${fmt(totalReturn - buyHoldReturn)}% |`,
|
|
714
|
+
`| Max Drawdown | -${fmt(maxDrawdown)}% |`,
|
|
715
|
+
`| Total Trades | ${trades.length} |`,
|
|
716
|
+
`| Win Rate | ${fmt(winRate)}% (${wins}/${sellTrades.length} sells) |`,
|
|
717
|
+
`| Remaining Cash | $${fmt(cash)} |`,
|
|
718
|
+
`| Holdings Value | $${fmt(holdings * finalPrice)} |`,
|
|
719
|
+
'',
|
|
720
|
+
`### Trade Log (last 10)`,
|
|
721
|
+
'',
|
|
722
|
+
'| # | Action | Price | Qty |',
|
|
723
|
+
'|---|--------|-------|-----|',
|
|
724
|
+
...trades.slice(-10).map((t, i) => `| ${trades.length - 10 + i + 1} | ${t.action} | $${fmt(t.price)} | ${fmt(t.qty, 6)} |`),
|
|
725
|
+
'',
|
|
726
|
+
`*Backtest uses historical data. Past performance ≠ future results.*`,
|
|
727
|
+
].join('\n');
|
|
728
|
+
},
|
|
729
|
+
});
|
|
730
|
+
// ─── Portfolio Rebalancer ───
|
|
731
|
+
registerTool({
|
|
732
|
+
name: 'portfolio_rebalance',
|
|
733
|
+
description: 'Analyze your paper portfolio and suggest rebalancing trades to match a target allocation. Helps maintain diversification.',
|
|
734
|
+
parameters: {
|
|
735
|
+
targets: { type: 'string', description: 'Target allocation as comma-separated pairs, e.g. "btc:50,eth:30,sol:20" (percentages must sum to 100)', required: true },
|
|
736
|
+
},
|
|
737
|
+
tier: 'free',
|
|
738
|
+
timeout: 15_000,
|
|
739
|
+
async execute(args) {
|
|
740
|
+
const portfolio = loadPortfolio();
|
|
741
|
+
const targetStr = String(args.targets);
|
|
742
|
+
// Parse targets
|
|
743
|
+
const targets = {};
|
|
744
|
+
let totalPct = 0;
|
|
745
|
+
for (const pair of targetStr.split(',')) {
|
|
746
|
+
const [sym, pct] = pair.trim().split(':');
|
|
747
|
+
if (!sym || !pct)
|
|
748
|
+
return `Invalid format. Use: "btc:50,eth:30,sol:20"`;
|
|
749
|
+
targets[sym.toLowerCase()] = Number(pct);
|
|
750
|
+
totalPct += Number(pct);
|
|
751
|
+
}
|
|
752
|
+
if (Math.abs(totalPct - 100) > 1)
|
|
753
|
+
return `Target percentages sum to ${totalPct}%, must be ~100%.`;
|
|
754
|
+
// Get current prices for all target tokens
|
|
755
|
+
const symbolMap = {
|
|
756
|
+
btc: 'bitcoin', eth: 'ethereum', sol: 'solana', bnb: 'binancecoin',
|
|
757
|
+
ada: 'cardano', doge: 'dogecoin', xrp: 'ripple',
|
|
758
|
+
};
|
|
759
|
+
const ids = Object.keys(targets).map(s => symbolMap[s] || s).join(',');
|
|
760
|
+
const priceData = await fetchJSON(`https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd`);
|
|
761
|
+
// Calculate current portfolio value
|
|
762
|
+
let totalValue = portfolio.cash;
|
|
763
|
+
const currentHoldings = {};
|
|
764
|
+
for (const pos of portfolio.positions) {
|
|
765
|
+
const cgId = symbolMap[pos.symbol] || pos.symbol;
|
|
766
|
+
const price = priceData[cgId]?.usd || pos.avgCost;
|
|
767
|
+
const value = pos.quantity * price;
|
|
768
|
+
totalValue += value;
|
|
769
|
+
currentHoldings[pos.symbol] = { qty: pos.quantity, value };
|
|
770
|
+
}
|
|
771
|
+
// Calculate rebalancing trades
|
|
772
|
+
const lines = [
|
|
773
|
+
'## Portfolio Rebalance Plan',
|
|
774
|
+
'',
|
|
775
|
+
`**Total Value**: $${fmt(totalValue)} (Cash: $${fmt(portfolio.cash)})`,
|
|
776
|
+
'',
|
|
777
|
+
'| Token | Current % | Target % | Action | Amount |',
|
|
778
|
+
'|-------|-----------|----------|--------|--------|',
|
|
779
|
+
];
|
|
780
|
+
const trades = [];
|
|
781
|
+
for (const [sym, targetPct] of Object.entries(targets)) {
|
|
782
|
+
const current = currentHoldings[sym];
|
|
783
|
+
const currentValue = current?.value || 0;
|
|
784
|
+
const currentPct = (currentValue / totalValue) * 100;
|
|
785
|
+
const targetValue = (targetPct / 100) * totalValue;
|
|
786
|
+
const diff = targetValue - currentValue;
|
|
787
|
+
let action = 'HOLD';
|
|
788
|
+
let amount = '';
|
|
789
|
+
if (Math.abs(diff) > totalValue * 0.02) { // Only suggest if >2% off
|
|
790
|
+
if (diff > 0) {
|
|
791
|
+
action = 'BUY';
|
|
792
|
+
amount = `$${fmt(diff)}`;
|
|
793
|
+
trades.push(`paper_trade buy ${sym} ${fmt(diff, 0)}`);
|
|
794
|
+
}
|
|
795
|
+
else {
|
|
796
|
+
action = 'SELL';
|
|
797
|
+
const cgId = symbolMap[sym] || sym;
|
|
798
|
+
const price = priceData[cgId]?.usd || 1;
|
|
799
|
+
const sellQty = Math.abs(diff) / price;
|
|
800
|
+
amount = `${fmt(sellQty, 4)} (≈$${fmt(Math.abs(diff))})`;
|
|
801
|
+
trades.push(`paper_trade sell ${sym} ${fmt(sellQty, 4)}`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
lines.push(`| ${sym.toUpperCase()} | ${fmt(currentPct)}% | ${fmt(targetPct)}% | ${action} | ${amount || '—'} |`);
|
|
805
|
+
}
|
|
806
|
+
if (trades.length) {
|
|
807
|
+
lines.push('', '### Suggested Trades', '');
|
|
808
|
+
for (const t of trades)
|
|
809
|
+
lines.push(`\`${t}\``);
|
|
810
|
+
}
|
|
811
|
+
else {
|
|
812
|
+
lines.push('', '*Portfolio is within 2% of targets. No rebalancing needed.*');
|
|
813
|
+
}
|
|
814
|
+
return lines.join('\n');
|
|
815
|
+
},
|
|
816
|
+
});
|
|
817
|
+
// ─── Trade Reasoning (Introspection) ───
|
|
818
|
+
registerTool({
|
|
819
|
+
name: 'trade_reasoning',
|
|
820
|
+
description: 'Walk through the full reasoning chain for a trade decision. Pulls all available signals (price, technicals, sentiment, news) and explains WHY each signal is bullish or bearish. The "show your work" tool.',
|
|
821
|
+
parameters: {
|
|
822
|
+
symbol: { type: 'string', description: 'Token symbol (e.g. "BTC", "ETH", "SOL")', required: true },
|
|
823
|
+
},
|
|
824
|
+
tier: 'free',
|
|
825
|
+
timeout: 30_000,
|
|
826
|
+
async execute(args) {
|
|
827
|
+
const symbol = String(args.symbol).toLowerCase();
|
|
828
|
+
const symbolMap = {
|
|
829
|
+
btc: 'bitcoin', eth: 'ethereum', sol: 'solana', bnb: 'binancecoin',
|
|
830
|
+
ada: 'cardano', doge: 'dogecoin', xrp: 'ripple',
|
|
831
|
+
};
|
|
832
|
+
const id = symbolMap[symbol] || symbol;
|
|
833
|
+
const signals = [];
|
|
834
|
+
// 1. Price trend
|
|
835
|
+
try {
|
|
836
|
+
const data = await fetchJSON(`https://api.coingecko.com/api/v3/coins/${id}?localization=false&tickers=false&community_data=false&developer_data=false`);
|
|
837
|
+
const market = data?.market_data;
|
|
838
|
+
if (market) {
|
|
839
|
+
const change24h = market.price_change_percentage_24h ?? 0;
|
|
840
|
+
const change7d = market.price_change_percentage_7d ?? 0;
|
|
841
|
+
const change30d = market.price_change_percentage_30d ?? 0;
|
|
842
|
+
const athChangePct = market.ath_change_percentage?.usd ?? 0;
|
|
843
|
+
let verdict = 'NEUTRAL';
|
|
844
|
+
let reasoning = '';
|
|
845
|
+
if (change7d > 10 && change30d > 20) {
|
|
846
|
+
verdict = 'BULLISH';
|
|
847
|
+
reasoning = `Strong uptrend: +${fmt(change7d)}% this week, +${fmt(change30d)}% this month. Momentum is sustained, not a single-day spike.`;
|
|
848
|
+
}
|
|
849
|
+
else if (change7d < -10 && change30d < -20) {
|
|
850
|
+
verdict = 'BEARISH';
|
|
851
|
+
reasoning = `Sustained decline: ${fmt(change7d)}% this week, ${fmt(change30d)}% this month. The selling pressure isn't letting up.`;
|
|
852
|
+
}
|
|
853
|
+
else if (change24h > 5 && change7d < 0) {
|
|
854
|
+
verdict = 'NEUTRAL';
|
|
855
|
+
reasoning = `Today's +${fmt(change24h)}% looks bullish, but the weekly trend is still ${fmt(change7d)}%. This could be a dead cat bounce or a reversal — need more confirmation.`;
|
|
856
|
+
}
|
|
857
|
+
else if (Math.abs(change7d) < 3) {
|
|
858
|
+
verdict = 'NEUTRAL';
|
|
859
|
+
reasoning = `Consolidating. Only ${fmt(change7d)}% movement this week. The market is deciding. Wait for a breakout in either direction.`;
|
|
860
|
+
}
|
|
861
|
+
else {
|
|
862
|
+
verdict = change7d > 0 ? 'BULLISH' : 'BEARISH';
|
|
863
|
+
reasoning = `${change7d > 0 ? 'Up' : 'Down'} ${fmt(Math.abs(change7d))}% this week. ${Math.abs(athChangePct) < 20 ? 'Near all-time high — momentum is strong but risk of correction increases.' : `${fmt(Math.abs(athChangePct))}% from ATH — room to run or further to fall.`}`;
|
|
864
|
+
}
|
|
865
|
+
signals.push({ name: 'Price Trend', verdict, reasoning, weight: 2 });
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
catch { /* skip */ }
|
|
869
|
+
// 2. Technical indicators
|
|
870
|
+
try {
|
|
871
|
+
const ohlc = await fetchJSON(`https://api.coingecko.com/api/v3/coins/${id}/ohlc?vs_currency=usd&days=90`);
|
|
872
|
+
if (ohlc?.length >= 30) {
|
|
873
|
+
const closes = ohlc.map(d => d[4]);
|
|
874
|
+
const rsiValues = rsi(closes, 14);
|
|
875
|
+
const currentRsi = rsiValues[rsiValues.length - 1];
|
|
876
|
+
const sma20vals = sma(closes, 20);
|
|
877
|
+
const sma50vals = sma(closes, Math.min(50, Math.floor(closes.length / 2)));
|
|
878
|
+
const current = closes[closes.length - 1];
|
|
879
|
+
const currentSma20 = sma20vals[sma20vals.length - 1];
|
|
880
|
+
const currentSma50 = sma50vals[sma50vals.length - 1];
|
|
881
|
+
// RSI reasoning
|
|
882
|
+
let rsiVerdict = 'NEUTRAL';
|
|
883
|
+
let rsiReasoning = '';
|
|
884
|
+
if (currentRsi > 70) {
|
|
885
|
+
rsiVerdict = 'BEARISH';
|
|
886
|
+
rsiReasoning = `RSI at ${fmt(currentRsi)} — overbought. When RSI exceeds 70, the asset has rallied hard and fast. Historically, pullbacks follow. Not a sell signal alone, but caution is warranted.`;
|
|
887
|
+
}
|
|
888
|
+
else if (currentRsi < 30) {
|
|
889
|
+
rsiVerdict = 'BULLISH';
|
|
890
|
+
rsiReasoning = `RSI at ${fmt(currentRsi)} — oversold. The selling has been extreme. Oversold RSI often precedes a bounce, especially if the 30d trend was up before this dip.`;
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
rsiReasoning = `RSI at ${fmt(currentRsi)} — neutral zone. No extreme signals. The market isn't overheated or panicked.`;
|
|
894
|
+
}
|
|
895
|
+
signals.push({ name: 'RSI (14)', verdict: rsiVerdict, reasoning: rsiReasoning, weight: 1.5 });
|
|
896
|
+
// Moving average reasoning
|
|
897
|
+
let maVerdict = 'NEUTRAL';
|
|
898
|
+
let maReasoning = '';
|
|
899
|
+
if (current > currentSma20 && currentSma20 > currentSma50) {
|
|
900
|
+
maVerdict = 'BULLISH';
|
|
901
|
+
maReasoning = `Price ($${fmt(current)}) > SMA20 ($${fmt(currentSma20)}) > SMA50 ($${fmt(currentSma50)}). This is a textbook uptrend structure. Each moving average is stacked above the longer one — the trend is intact.`;
|
|
902
|
+
}
|
|
903
|
+
else if (current < currentSma20 && currentSma20 < currentSma50) {
|
|
904
|
+
maVerdict = 'BEARISH';
|
|
905
|
+
maReasoning = `Price ($${fmt(current)}) < SMA20 ($${fmt(currentSma20)}) < SMA50 ($${fmt(currentSma50)}). Downtrend structure. Price is below all moving averages — sellers are in control.`;
|
|
906
|
+
}
|
|
907
|
+
else {
|
|
908
|
+
maReasoning = `Mixed signals. Price is ${current > currentSma20 ? 'above' : 'below'} the 20-day MA but ${current > currentSma50 ? 'above' : 'below'} the 50-day. The trend is transitioning — could go either way.`;
|
|
909
|
+
}
|
|
910
|
+
signals.push({ name: 'Moving Averages', verdict: maVerdict, reasoning: maReasoning, weight: 1.5 });
|
|
911
|
+
// Bollinger reasoning
|
|
912
|
+
const bb = bollingerBands(closes, 20);
|
|
913
|
+
const bbUpper = bb.upper[bb.upper.length - 1];
|
|
914
|
+
const bbLower = bb.lower[bb.lower.length - 1];
|
|
915
|
+
let bbVerdict = 'NEUTRAL';
|
|
916
|
+
let bbReasoning = '';
|
|
917
|
+
if (current > bbUpper) {
|
|
918
|
+
bbVerdict = 'BEARISH';
|
|
919
|
+
bbReasoning = `Price above the upper Bollinger Band ($${fmt(bbUpper)}). The asset is trading beyond 2 standard deviations from the mean — statistically, it tends to revert. But in strong trends, prices can "ride the band" for weeks.`;
|
|
920
|
+
}
|
|
921
|
+
else if (current < bbLower) {
|
|
922
|
+
bbVerdict = 'BULLISH';
|
|
923
|
+
bbReasoning = `Price below the lower Bollinger Band ($${fmt(bbLower)}). This is a statistical extreme — price is more than 2 standard deviations below the mean. Mean reversion is likely, but confirm with volume.`;
|
|
924
|
+
}
|
|
925
|
+
else {
|
|
926
|
+
const position = ((current - bbLower) / (bbUpper - bbLower)) * 100;
|
|
927
|
+
bbReasoning = `Price at ${fmt(position)}% of the Bollinger range. ${position > 70 ? 'Approaching the upper band — watch for resistance.' : position < 30 ? 'Near the lower band — potential support.' : 'Middle of the range — no extremes.'}`;
|
|
928
|
+
}
|
|
929
|
+
signals.push({ name: 'Bollinger Bands', verdict: bbVerdict, reasoning: bbReasoning, weight: 1 });
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
catch { /* skip */ }
|
|
933
|
+
// 3. Fear & Greed
|
|
934
|
+
try {
|
|
935
|
+
const fg = await fetchJSON('https://api.alternative.me/fng/?limit=1');
|
|
936
|
+
if (fg.data?.[0]) {
|
|
937
|
+
const value = Number(fg.data[0].value);
|
|
938
|
+
const label = fg.data[0].value_classification;
|
|
939
|
+
let verdict = 'NEUTRAL';
|
|
940
|
+
let reasoning = '';
|
|
941
|
+
if (value <= 25) {
|
|
942
|
+
verdict = 'BULLISH';
|
|
943
|
+
reasoning = `Fear & Greed at ${value} (${label}). Warren Buffett's rule: "Be greedy when others are fearful." Extreme fear often marks bottoms — but fear can persist longer than expected.`;
|
|
944
|
+
}
|
|
945
|
+
else if (value >= 75) {
|
|
946
|
+
verdict = 'BEARISH';
|
|
947
|
+
reasoning = `Fear & Greed at ${value} (${label}). "Be fearful when others are greedy." Euphoria is high — this is when smart money starts taking profits. Doesn't mean crash is imminent, but risk is elevated.`;
|
|
948
|
+
}
|
|
949
|
+
else {
|
|
950
|
+
reasoning = `Fear & Greed at ${value} (${label}). Neither extreme. The market is rational right now — decisions should be based on fundamentals and technicals, not crowd psychology.`;
|
|
951
|
+
}
|
|
952
|
+
signals.push({ name: 'Fear & Greed', verdict, reasoning, weight: 1 });
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
catch { /* skip */ }
|
|
956
|
+
if (!signals.length)
|
|
957
|
+
return `Could not gather signals for "${symbol}". Check the symbol and try again.`;
|
|
958
|
+
// Synthesize
|
|
959
|
+
let weightedBullish = 0;
|
|
960
|
+
let weightedBearish = 0;
|
|
961
|
+
let totalWeight = 0;
|
|
962
|
+
for (const s of signals) {
|
|
963
|
+
totalWeight += s.weight;
|
|
964
|
+
if (s.verdict === 'BULLISH')
|
|
965
|
+
weightedBullish += s.weight;
|
|
966
|
+
if (s.verdict === 'BEARISH')
|
|
967
|
+
weightedBearish += s.weight;
|
|
968
|
+
}
|
|
969
|
+
const bullishPct = Math.round((weightedBullish / totalWeight) * 100);
|
|
970
|
+
const bearishPct = Math.round((weightedBearish / totalWeight) * 100);
|
|
971
|
+
const neutralPct = 100 - bullishPct - bearishPct;
|
|
972
|
+
const overall = bullishPct > bearishPct + 15 ? 'BULLISH'
|
|
973
|
+
: bearishPct > bullishPct + 15 ? 'BEARISH'
|
|
974
|
+
: 'NEUTRAL';
|
|
975
|
+
const lines = [
|
|
976
|
+
`## Trade Reasoning: ${symbol.toUpperCase()}`,
|
|
977
|
+
'',
|
|
978
|
+
`### Verdict: **${overall}** (${bullishPct}% bullish / ${bearishPct}% bearish / ${neutralPct}% neutral)`,
|
|
979
|
+
'',
|
|
980
|
+
];
|
|
981
|
+
for (const s of signals) {
|
|
982
|
+
const icon = s.verdict === 'BULLISH' ? '🟢' : s.verdict === 'BEARISH' ? '🔴' : '⚪';
|
|
983
|
+
lines.push(`### ${icon} ${s.name}: ${s.verdict}`);
|
|
984
|
+
lines.push('');
|
|
985
|
+
lines.push(s.reasoning);
|
|
986
|
+
lines.push('');
|
|
987
|
+
}
|
|
988
|
+
lines.push('---');
|
|
989
|
+
lines.push('');
|
|
990
|
+
if (overall === 'BULLISH') {
|
|
991
|
+
lines.push(`**What this means**: The majority of signals point up. If you\'re looking to enter, this is a favorable setup — but always use a stop loss. Consider a staged entry (buy 50% now, 50% on a dip).`);
|
|
992
|
+
}
|
|
993
|
+
else if (overall === 'BEARISH') {
|
|
994
|
+
lines.push(`**What this means**: More signals point down than up. If you\'re holding, consider tightening your stop loss. If you\'re looking to enter, wait for a clearer signal — catching a falling knife is expensive.`);
|
|
995
|
+
}
|
|
996
|
+
else {
|
|
997
|
+
lines.push(`**What this means**: Signals are mixed. The market hasn\'t decided yet. This is a wait-and-see moment. Set price alerts at key levels and let the market come to you.`);
|
|
998
|
+
}
|
|
999
|
+
lines.push('');
|
|
1000
|
+
lines.push(`*${signals.length} signals analyzed. Weighted by reliability. Not financial advice.*`);
|
|
1001
|
+
return lines.join('\n');
|
|
1002
|
+
},
|
|
1003
|
+
});
|
|
1004
|
+
// ─── Price Alerts ───
|
|
1005
|
+
registerTool({
|
|
1006
|
+
name: 'price_alert',
|
|
1007
|
+
description: 'Set, check, or clear price alerts for crypto tokens. Alerts are stored locally and checked whenever you run this tool.',
|
|
1008
|
+
parameters: {
|
|
1009
|
+
action: { type: 'string', description: '"set", "check", "list", or "clear"', required: true },
|
|
1010
|
+
symbol: { type: 'string', description: 'Token symbol (for set/clear)' },
|
|
1011
|
+
above: { type: 'number', description: 'Alert when price goes above this (for set)' },
|
|
1012
|
+
below: { type: 'number', description: 'Alert when price goes below this (for set)' },
|
|
1013
|
+
},
|
|
1014
|
+
tier: 'free',
|
|
1015
|
+
timeout: 15_000,
|
|
1016
|
+
async execute(args) {
|
|
1017
|
+
const action = String(args.action).toLowerCase();
|
|
1018
|
+
const alertPath = join(homedir(), '.kbot', 'price-alerts.json');
|
|
1019
|
+
const loadAlerts = () => {
|
|
1020
|
+
if (!existsSync(alertPath))
|
|
1021
|
+
return [];
|
|
1022
|
+
try {
|
|
1023
|
+
return JSON.parse(readFileSync(alertPath, 'utf-8'));
|
|
1024
|
+
}
|
|
1025
|
+
catch {
|
|
1026
|
+
return [];
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
const saveAlerts = (a) => {
|
|
1030
|
+
if (!existsSync(join(homedir(), '.kbot')))
|
|
1031
|
+
mkdirSync(join(homedir(), '.kbot'), { recursive: true });
|
|
1032
|
+
writeFileSync(alertPath, JSON.stringify(a, null, 2));
|
|
1033
|
+
};
|
|
1034
|
+
if (action === 'set') {
|
|
1035
|
+
const symbol = String(args.symbol || '').toLowerCase();
|
|
1036
|
+
if (!symbol)
|
|
1037
|
+
return 'Error: symbol required.';
|
|
1038
|
+
const above = args.above ? Number(args.above) : undefined;
|
|
1039
|
+
const below = args.below ? Number(args.below) : undefined;
|
|
1040
|
+
if (!above && !below)
|
|
1041
|
+
return 'Error: set at least one of "above" or "below".';
|
|
1042
|
+
const alerts = loadAlerts();
|
|
1043
|
+
alerts.push({ symbol, above, below, createdAt: new Date().toISOString() });
|
|
1044
|
+
saveAlerts(alerts);
|
|
1045
|
+
const parts = [];
|
|
1046
|
+
if (above)
|
|
1047
|
+
parts.push(`above $${fmt(above)}`);
|
|
1048
|
+
if (below)
|
|
1049
|
+
parts.push(`below $${fmt(below)}`);
|
|
1050
|
+
return `**Alert set**: ${symbol.toUpperCase()} — trigger when price goes ${parts.join(' or ')}.`;
|
|
1051
|
+
}
|
|
1052
|
+
if (action === 'list') {
|
|
1053
|
+
const alerts = loadAlerts();
|
|
1054
|
+
if (!alerts.length)
|
|
1055
|
+
return 'No active alerts. Use `price_alert set` to create one.';
|
|
1056
|
+
const lines = ['## Active Price Alerts', '', '| # | Token | Above | Below | Set |', '|---|-------|-------|-------|-----|'];
|
|
1057
|
+
alerts.forEach((a, i) => {
|
|
1058
|
+
lines.push(`| ${i + 1} | ${a.symbol.toUpperCase()} | ${a.above ? `$${fmt(a.above)}` : '—'} | ${a.below ? `$${fmt(a.below)}` : '—'} | ${a.createdAt.split('T')[0]} |`);
|
|
1059
|
+
});
|
|
1060
|
+
return lines.join('\n');
|
|
1061
|
+
}
|
|
1062
|
+
if (action === 'clear') {
|
|
1063
|
+
const symbol = String(args.symbol || '').toLowerCase();
|
|
1064
|
+
const alerts = loadAlerts();
|
|
1065
|
+
if (symbol) {
|
|
1066
|
+
const filtered = alerts.filter(a => a.symbol !== symbol);
|
|
1067
|
+
saveAlerts(filtered);
|
|
1068
|
+
return `Cleared ${alerts.length - filtered.length} alert(s) for ${symbol.toUpperCase()}.`;
|
|
1069
|
+
}
|
|
1070
|
+
saveAlerts([]);
|
|
1071
|
+
return `Cleared all ${alerts.length} alert(s).`;
|
|
1072
|
+
}
|
|
1073
|
+
if (action === 'check') {
|
|
1074
|
+
const alerts = loadAlerts();
|
|
1075
|
+
if (!alerts.length)
|
|
1076
|
+
return 'No alerts to check.';
|
|
1077
|
+
// Get all unique symbols
|
|
1078
|
+
const symbols = [...new Set(alerts.map(a => a.symbol))];
|
|
1079
|
+
const symbolMap = {
|
|
1080
|
+
btc: 'bitcoin', eth: 'ethereum', sol: 'solana', bnb: 'binancecoin',
|
|
1081
|
+
ada: 'cardano', doge: 'dogecoin', xrp: 'ripple',
|
|
1082
|
+
};
|
|
1083
|
+
const ids = symbols.map(s => symbolMap[s] || s).join(',');
|
|
1084
|
+
const priceData = await fetchJSON(`https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd`);
|
|
1085
|
+
const triggered = [];
|
|
1086
|
+
const remaining = [];
|
|
1087
|
+
for (const alert of alerts) {
|
|
1088
|
+
const cgId = symbolMap[alert.symbol] || alert.symbol;
|
|
1089
|
+
const price = priceData[cgId]?.usd;
|
|
1090
|
+
if (!price) {
|
|
1091
|
+
remaining.push(alert);
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
let fired = false;
|
|
1095
|
+
if (alert.above && price >= alert.above) {
|
|
1096
|
+
triggered.push(`**${alert.symbol.toUpperCase()}** hit $${fmt(price)} (above $${fmt(alert.above)})`);
|
|
1097
|
+
fired = true;
|
|
1098
|
+
}
|
|
1099
|
+
if (alert.below && price <= alert.below) {
|
|
1100
|
+
triggered.push(`**${alert.symbol.toUpperCase()}** hit $${fmt(price)} (below $${fmt(alert.below)})`);
|
|
1101
|
+
fired = true;
|
|
1102
|
+
}
|
|
1103
|
+
if (!fired)
|
|
1104
|
+
remaining.push(alert);
|
|
1105
|
+
}
|
|
1106
|
+
saveAlerts(remaining); // Remove triggered alerts
|
|
1107
|
+
if (triggered.length) {
|
|
1108
|
+
return ['## 🔔 Triggered Alerts', '', ...triggered, '', `${remaining.length} alert(s) still active.`].join('\n');
|
|
1109
|
+
}
|
|
1110
|
+
return `No alerts triggered. ${remaining.length} alert(s) still active. All prices within thresholds.`;
|
|
1111
|
+
}
|
|
1112
|
+
return 'Unknown action. Use: set, check, list, or clear.';
|
|
1113
|
+
},
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
//# sourceMappingURL=finance.js.map
|