@zhive/cli 0.6.6 → 0.6.8
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/commands/agent/commands/profile.js +3 -15
- package/dist/commands/agent/commands/profile.test.js +2 -23
- package/dist/commands/create/presets/index.js +1 -1
- package/dist/commands/create/presets/options.js +18 -15
- package/dist/commands/create/ui/steps/IdentityStep.js +3 -2
- package/dist/commands/doctor/commands/index.js +5 -11
- package/dist/commands/indicator/commands/bollinger.js +37 -0
- package/dist/commands/indicator/commands/ema.js +37 -0
- package/dist/commands/indicator/commands/index.js +14 -0
- package/dist/commands/indicator/commands/macd.js +51 -0
- package/dist/commands/indicator/commands/rsi.js +37 -0
- package/dist/commands/indicator/commands/sma.js +37 -0
- package/dist/commands/market/commands/index.js +5 -0
- package/dist/commands/market/commands/price.js +25 -0
- package/dist/commands/megathread/commands/create-comment.js +2 -7
- package/dist/commands/megathread/commands/create-comment.test.js +3 -30
- package/dist/commands/megathread/commands/create-comments.js +2 -7
- package/dist/commands/megathread/commands/list.js +5 -10
- package/dist/commands/megathread/commands/list.test.js +3 -21
- package/dist/commands/migrate-templates/ui/MigrateApp.js +1 -1
- package/dist/commands/shared/utils.js +12 -0
- package/dist/commands/start/commands/prediction.js +1 -1
- package/dist/commands/start/commands/skills.test.js +1 -2
- package/dist/components/MultiSelectPrompt.js +3 -3
- package/dist/index.js +4 -0
- package/dist/shared/agent/analysis.js +2 -12
- package/dist/shared/agent/cache.js +10 -0
- package/dist/shared/agent/handler.js +3 -9
- package/dist/shared/agent/prompts/megathread.js +0 -8
- package/dist/shared/agent/tools/execute-skill-tool.js +2 -1
- package/dist/shared/agent/tools/formatting.js +0 -19
- package/dist/shared/agent/tools/market/client.js +3 -3
- package/dist/shared/agent/tools/market/tools.js +88 -312
- package/dist/shared/agent/tools/market/utils.js +71 -0
- package/dist/shared/agent/tools/mindshare/tools.js +1 -1
- package/dist/shared/agent/tools/ta/index.js +3 -1
- package/dist/shared/agent/utils.js +44 -0
- package/dist/shared/config/agent.js +4 -0
- package/dist/shared/config/agent.test.js +0 -5
- package/dist/shared/ta/error.js +12 -0
- package/dist/shared/ta/service.js +93 -0
- package/dist/shared/ta/utils.js +16 -0
- package/package.json +2 -1
|
@@ -1,24 +1,9 @@
|
|
|
1
1
|
import { tool } from 'ai';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
* Adjusts the 'from' date backwards to ensure sufficient data points are fetched
|
|
8
|
-
* for indicator calculation. Adds a 30% buffer for weekends/gaps in data.
|
|
9
|
-
*/
|
|
10
|
-
function adjustFromDate(from, minPoints, interval) {
|
|
11
|
-
const fromDate = new Date(from);
|
|
12
|
-
const buffer = Math.ceil(minPoints * 0.3);
|
|
13
|
-
const totalPoints = minPoints + buffer;
|
|
14
|
-
if (interval === 'hourly') {
|
|
15
|
-
fromDate.setTime(fromDate.getTime() - totalPoints * 60 * 60 * 1000);
|
|
16
|
-
}
|
|
17
|
-
else {
|
|
18
|
-
fromDate.setTime(fromDate.getTime() - totalPoints * 24 * 60 * 60 * 1000);
|
|
19
|
-
}
|
|
20
|
-
return fromDate.toISOString();
|
|
21
|
-
}
|
|
3
|
+
import { InsufficientDataError } from '../../../ta/error.js';
|
|
4
|
+
import { getBollingerBands, getEMA, getMACD, getOHLC, getPrice, getRSI, getSMA, } from '../../../ta/service.js';
|
|
5
|
+
import { formatBollingerData, formatIndicatorData, formatMACDData, formatOhlcData, } from './utils.js';
|
|
6
|
+
import { formatToolError } from '../../utils.js';
|
|
22
7
|
const timeRangeSchema = z.object({
|
|
23
8
|
from: z
|
|
24
9
|
.string()
|
|
@@ -39,23 +24,13 @@ export const getPriceTool = tool({
|
|
|
39
24
|
.describe('Optional timestamp in ISO 8601 format. If omitted, returns the current price.'),
|
|
40
25
|
}),
|
|
41
26
|
execute: async ({ projectId, timestamp }) => {
|
|
27
|
+
const effectiveTimestamp = timestamp ?? new Date().toISOString();
|
|
42
28
|
try {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
const priceData = await client.getPrice(projectId, effectiveTimestamp);
|
|
46
|
-
if (priceData.price === null) {
|
|
29
|
+
const price = await getPrice({ project: projectId, at: effectiveTimestamp });
|
|
30
|
+
if (price === undefined) {
|
|
47
31
|
return `No price data available for ${projectId} at ${effectiveTimestamp}.`;
|
|
48
32
|
}
|
|
49
|
-
|
|
50
|
-
const priceFormatted = price.toLocaleString('en-US', {
|
|
51
|
-
style: 'currency',
|
|
52
|
-
currency: 'USD',
|
|
53
|
-
minimumFractionDigits: 2,
|
|
54
|
-
maximumFractionDigits: price < 1 ? 6 : 2,
|
|
55
|
-
});
|
|
56
|
-
const timeLabel = timestamp ? `at ${timestamp}` : '(current)';
|
|
57
|
-
const output = `${projectId}: ${priceFormatted} ${timeLabel}`;
|
|
58
|
-
return output;
|
|
33
|
+
return price;
|
|
59
34
|
}
|
|
60
35
|
catch (err) {
|
|
61
36
|
return formatToolError(err, 'fetching price');
|
|
@@ -74,38 +49,17 @@ export const getOHLCTool = tool({
|
|
|
74
49
|
}),
|
|
75
50
|
execute: async ({ id, from, to, interval }) => {
|
|
76
51
|
try {
|
|
77
|
-
const client = getMarketClient();
|
|
78
52
|
const effectiveInterval = interval ?? 'daily';
|
|
79
|
-
const ohlcData = await
|
|
53
|
+
const ohlcData = await getOHLC({
|
|
54
|
+
project: id,
|
|
55
|
+
from,
|
|
56
|
+
to,
|
|
57
|
+
interval: effectiveInterval,
|
|
58
|
+
});
|
|
80
59
|
if (ohlcData.length === 0) {
|
|
81
60
|
return `No OHLC data available for ${id} from ${from} to ${to}.`;
|
|
82
61
|
}
|
|
83
|
-
|
|
84
|
-
`OHLC data for ${id} (${effectiveInterval}, ${ohlcData.length} data points):`,
|
|
85
|
-
'',
|
|
86
|
-
'Date | Open | High | Low | Close',
|
|
87
|
-
'--- | --- | --- | --- | ---',
|
|
88
|
-
];
|
|
89
|
-
const displayData = truncateTimeseries(ohlcData, 30, 10, 10);
|
|
90
|
-
for (const point of displayData) {
|
|
91
|
-
if (point === null) {
|
|
92
|
-
lines.push(truncationLabel(ohlcData.length, 20));
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
const date = new Date(point[0]).toISOString().split('T')[0];
|
|
96
|
-
const open = point[1].toFixed(2);
|
|
97
|
-
const high = point[2].toFixed(2);
|
|
98
|
-
const low = point[3].toFixed(2);
|
|
99
|
-
const close = point[4].toFixed(2);
|
|
100
|
-
lines.push(`${date} | $${open} | $${high} | $${low} | $${close}`);
|
|
101
|
-
}
|
|
102
|
-
const firstClose = ohlcData[0][4];
|
|
103
|
-
const lastClose = ohlcData[ohlcData.length - 1][4];
|
|
104
|
-
const changePercent = ((lastClose - firstClose) / firstClose) * 100;
|
|
105
|
-
lines.push('');
|
|
106
|
-
lines.push(`Period change: ${signPrefix(changePercent)}${changePercent.toFixed(2)}% ($${firstClose.toFixed(2)} → $${lastClose.toFixed(2)})`);
|
|
107
|
-
const output = lines.join('\n');
|
|
108
|
-
return output;
|
|
62
|
+
return formatOhlcData(ohlcData);
|
|
109
63
|
}
|
|
110
64
|
catch (err) {
|
|
111
65
|
return formatToolError(err, 'fetching OHLC data');
|
|
@@ -130,42 +84,20 @@ export const getSMATool = tool({
|
|
|
130
84
|
}),
|
|
131
85
|
execute: async ({ id, period, from, to, interval }) => {
|
|
132
86
|
try {
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (smaData.length === 0) {
|
|
142
|
-
return `Could not compute SMA-${period} for ${id}.`;
|
|
143
|
-
}
|
|
144
|
-
// Filter results to original date range
|
|
145
|
-
const originalFromTime = new Date(from).getTime();
|
|
146
|
-
const filteredData = smaData.filter((point) => point.timestamp >= originalFromTime);
|
|
147
|
-
if (filteredData.length === 0) {
|
|
148
|
-
return `No SMA-${period} data available within the requested date range for ${id}.`;
|
|
149
|
-
}
|
|
150
|
-
const lines = [
|
|
151
|
-
`SMA-${period} for ${id} (${effectiveInterval}, ${filteredData.length} values):`,
|
|
152
|
-
'',
|
|
153
|
-
];
|
|
154
|
-
for (const point of filteredData) {
|
|
155
|
-
const date = new Date(point.timestamp).toISOString().split('T')[0];
|
|
156
|
-
lines.push(`${date}: $${point.value.toFixed(2)}`);
|
|
157
|
-
}
|
|
158
|
-
const latestSMA = filteredData[filteredData.length - 1].value;
|
|
159
|
-
const latestPrice = ohlcData[ohlcData.length - 1][4];
|
|
160
|
-
const aboveBelow = latestPrice > latestSMA ? 'above' : 'below';
|
|
161
|
-
const diff = ((latestPrice - latestSMA) / latestSMA) * 100;
|
|
162
|
-
lines.push('');
|
|
163
|
-
lines.push(`Current price ($${latestPrice.toFixed(2)}) is ${aboveBelow} SMA-${period} ($${latestSMA.toFixed(2)}) by ${signPrefix(diff)}${diff.toFixed(2)}%`);
|
|
164
|
-
const output = lines.join('\n');
|
|
165
|
-
return output;
|
|
87
|
+
const smaResult = await getSMA({
|
|
88
|
+
project: id,
|
|
89
|
+
interval,
|
|
90
|
+
from,
|
|
91
|
+
to,
|
|
92
|
+
period,
|
|
93
|
+
});
|
|
94
|
+
return formatIndicatorData(smaResult);
|
|
166
95
|
}
|
|
167
|
-
catch (
|
|
168
|
-
|
|
96
|
+
catch (e) {
|
|
97
|
+
if (e instanceof InsufficientDataError) {
|
|
98
|
+
return `Insufficient data: got ${e.got} data points but need at least ${e.required} for SMA${period}.`;
|
|
99
|
+
}
|
|
100
|
+
return formatToolError(e, 'calculating SMA');
|
|
169
101
|
}
|
|
170
102
|
},
|
|
171
103
|
});
|
|
@@ -187,42 +119,20 @@ export const getEMATool = tool({
|
|
|
187
119
|
}),
|
|
188
120
|
execute: async ({ id, period, from, to, interval }) => {
|
|
189
121
|
try {
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (emaData.length === 0) {
|
|
199
|
-
return `Could not compute EMA-${period} for ${id}.`;
|
|
200
|
-
}
|
|
201
|
-
// Filter results to original date range
|
|
202
|
-
const originalFromTime = new Date(from).getTime();
|
|
203
|
-
const filteredData = emaData.filter((point) => point.timestamp >= originalFromTime);
|
|
204
|
-
if (filteredData.length === 0) {
|
|
205
|
-
return `No EMA-${period} data available within the requested date range for ${id}.`;
|
|
206
|
-
}
|
|
207
|
-
const lines = [
|
|
208
|
-
`EMA-${period} for ${id} (${effectiveInterval}, ${filteredData.length} values):`,
|
|
209
|
-
'',
|
|
210
|
-
];
|
|
211
|
-
for (const point of filteredData) {
|
|
212
|
-
const date = new Date(point.timestamp).toISOString().split('T')[0];
|
|
213
|
-
lines.push(`${date}: $${point.value.toFixed(2)}`);
|
|
214
|
-
}
|
|
215
|
-
const latestEMA = filteredData[filteredData.length - 1].value;
|
|
216
|
-
const latestPrice = ohlcData[ohlcData.length - 1][4];
|
|
217
|
-
const aboveBelow = latestPrice > latestEMA ? 'above' : 'below';
|
|
218
|
-
const diff = ((latestPrice - latestEMA) / latestEMA) * 100;
|
|
219
|
-
lines.push('');
|
|
220
|
-
lines.push(`Current price ($${latestPrice.toFixed(2)}) is ${aboveBelow} EMA-${period} ($${latestEMA.toFixed(2)}) by ${signPrefix(diff)}${diff.toFixed(2)}%`);
|
|
221
|
-
const output = lines.join('\n');
|
|
222
|
-
return output;
|
|
122
|
+
const emaResult = await getEMA({
|
|
123
|
+
project: id,
|
|
124
|
+
interval,
|
|
125
|
+
from,
|
|
126
|
+
to,
|
|
127
|
+
period,
|
|
128
|
+
});
|
|
129
|
+
return formatIndicatorData(emaResult);
|
|
223
130
|
}
|
|
224
|
-
catch (
|
|
225
|
-
|
|
131
|
+
catch (e) {
|
|
132
|
+
if (e instanceof InsufficientDataError) {
|
|
133
|
+
return `Insufficient data: got ${e.got} data points but need at least ${e.required} for EMA${period}.`;
|
|
134
|
+
}
|
|
135
|
+
return formatToolError(e, 'calculating EMA');
|
|
226
136
|
}
|
|
227
137
|
},
|
|
228
138
|
});
|
|
@@ -238,66 +148,22 @@ export const getRSITool = tool({
|
|
|
238
148
|
.describe('Data interval: "daily" or "hourly". Defaults to "daily".'),
|
|
239
149
|
}),
|
|
240
150
|
execute: async ({ id, period, from, to, interval }) => {
|
|
151
|
+
const effectivePeriod = period ?? 14;
|
|
241
152
|
try {
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
const rsiData = computeRSI(ohlcData, effectivePeriod);
|
|
252
|
-
if (rsiData.length === 0) {
|
|
253
|
-
return `Could not compute RSI-${effectivePeriod} for ${id}.`;
|
|
254
|
-
}
|
|
255
|
-
// Filter results to original date range
|
|
256
|
-
const originalFromTime = new Date(from).getTime();
|
|
257
|
-
const filteredData = rsiData.filter((point) => point.timestamp >= originalFromTime);
|
|
258
|
-
if (filteredData.length === 0) {
|
|
259
|
-
return `No RSI-${effectivePeriod} data available within the requested date range for ${id}.`;
|
|
260
|
-
}
|
|
261
|
-
const lines = [
|
|
262
|
-
`RSI-${effectivePeriod} for ${id} (${effectiveInterval}, ${filteredData.length} values):`,
|
|
263
|
-
'',
|
|
264
|
-
];
|
|
265
|
-
for (const point of filteredData) {
|
|
266
|
-
const date = new Date(point.timestamp).toISOString().split('T')[0];
|
|
267
|
-
const rsiValue = point.value.toFixed(2);
|
|
268
|
-
let status = '';
|
|
269
|
-
if (point.value >= 70) {
|
|
270
|
-
status = ' [OVERBOUGHT]';
|
|
271
|
-
}
|
|
272
|
-
else if (point.value <= 30) {
|
|
273
|
-
status = ' [OVERSOLD]';
|
|
274
|
-
}
|
|
275
|
-
lines.push(`${date}: ${rsiValue}${status}`);
|
|
276
|
-
}
|
|
277
|
-
const latestRSI = filteredData[filteredData.length - 1].value;
|
|
278
|
-
let interpretation = '';
|
|
279
|
-
if (latestRSI >= 70) {
|
|
280
|
-
interpretation =
|
|
281
|
-
'The RSI is in overbought territory (>=70), which may indicate the asset is overvalued and could see a pullback.';
|
|
282
|
-
}
|
|
283
|
-
else if (latestRSI <= 30) {
|
|
284
|
-
interpretation =
|
|
285
|
-
'The RSI is in oversold territory (<=30), which may indicate the asset is undervalued and could see a bounce.';
|
|
286
|
-
}
|
|
287
|
-
else if (latestRSI >= 50) {
|
|
288
|
-
interpretation = 'The RSI shows bullish momentum (above 50).';
|
|
289
|
-
}
|
|
290
|
-
else {
|
|
291
|
-
interpretation = 'The RSI shows bearish momentum (below 50).';
|
|
292
|
-
}
|
|
293
|
-
lines.push('');
|
|
294
|
-
lines.push(`Current RSI: ${latestRSI.toFixed(2)}`);
|
|
295
|
-
lines.push(interpretation);
|
|
296
|
-
const output = lines.join('\n');
|
|
297
|
-
return output;
|
|
153
|
+
const rsi = await getRSI({
|
|
154
|
+
project: id,
|
|
155
|
+
interval,
|
|
156
|
+
from,
|
|
157
|
+
to,
|
|
158
|
+
period: effectivePeriod,
|
|
159
|
+
});
|
|
160
|
+
return formatIndicatorData(rsi);
|
|
298
161
|
}
|
|
299
|
-
catch (
|
|
300
|
-
|
|
162
|
+
catch (e) {
|
|
163
|
+
if (e instanceof InsufficientDataError) {
|
|
164
|
+
return `Insufficient data: got ${e.got} data points but need at least ${e.required} for RSI${effectivePeriod}.`;
|
|
165
|
+
}
|
|
166
|
+
return formatToolError(e, 'calculating RSI');
|
|
301
167
|
}
|
|
302
168
|
},
|
|
303
169
|
});
|
|
@@ -333,65 +199,26 @@ export const getMACDTool = tool({
|
|
|
333
199
|
.describe('Data interval: "daily" or "hourly". Defaults to "daily".'),
|
|
334
200
|
}),
|
|
335
201
|
execute: async ({ id, fastPeriod, slowPeriod, signalPeriod, from, to, interval }) => {
|
|
202
|
+
const fast = fastPeriod ?? 12;
|
|
203
|
+
const slow = slowPeriod ?? 26;
|
|
204
|
+
const signal = signalPeriod ?? 9;
|
|
336
205
|
try {
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
348
|
-
const macdData = computeMACD(ohlcData, fast, slow, signal);
|
|
349
|
-
if (macdData.length === 0) {
|
|
350
|
-
return `Could not compute MACD(${fast},${slow},${signal}) for ${id}.`;
|
|
351
|
-
}
|
|
352
|
-
// Filter results to original date range
|
|
353
|
-
const originalFromTime = new Date(from).getTime();
|
|
354
|
-
const filteredData = macdData.filter((point) => point.timestamp >= originalFromTime);
|
|
355
|
-
if (filteredData.length === 0) {
|
|
356
|
-
return `No MACD(${fast},${slow},${signal}) data available within the requested date range for ${id}.`;
|
|
357
|
-
}
|
|
358
|
-
const lines = [
|
|
359
|
-
`MACD(${fast},${slow},${signal}) for ${id} (${effectiveInterval}, ${filteredData.length} values):`,
|
|
360
|
-
'',
|
|
361
|
-
'Date | MACD | Signal | Histogram',
|
|
362
|
-
'--- | --- | --- | ---',
|
|
363
|
-
];
|
|
364
|
-
for (const point of filteredData) {
|
|
365
|
-
const date = new Date(point.timestamp).toISOString().split('T')[0];
|
|
366
|
-
const macdVal = point.macd.toFixed(4);
|
|
367
|
-
const signalVal = point.signal.toFixed(4);
|
|
368
|
-
const histVal = point.histogram.toFixed(4);
|
|
369
|
-
lines.push(`${date} | ${macdVal} | ${signalVal} | ${signPrefix(point.histogram)}${histVal}`);
|
|
370
|
-
}
|
|
371
|
-
const latest = filteredData[filteredData.length - 1];
|
|
372
|
-
const previous = filteredData.length > 1 ? filteredData[filteredData.length - 2] : null;
|
|
373
|
-
let crossover = '';
|
|
374
|
-
if (previous !== null) {
|
|
375
|
-
if (previous.macd <= previous.signal && latest.macd > latest.signal) {
|
|
376
|
-
crossover = 'BULLISH CROSSOVER detected (MACD crossed above Signal line)!';
|
|
377
|
-
}
|
|
378
|
-
else if (previous.macd >= previous.signal && latest.macd < latest.signal) {
|
|
379
|
-
crossover = 'BEARISH CROSSOVER detected (MACD crossed below Signal line)!';
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
const trend = latest.macd > latest.signal ? 'bullish (MACD above signal)' : 'bearish (MACD below signal)';
|
|
383
|
-
const momentum = latest.histogram > 0 ? 'positive' : 'negative';
|
|
384
|
-
lines.push('');
|
|
385
|
-
lines.push(`Current trend: ${trend}`);
|
|
386
|
-
lines.push(`Histogram momentum: ${momentum}`);
|
|
387
|
-
if (crossover) {
|
|
388
|
-
lines.push(crossover);
|
|
389
|
-
}
|
|
390
|
-
const output = lines.join('\n');
|
|
391
|
-
return output;
|
|
206
|
+
const macdResult = await getMACD({
|
|
207
|
+
project: id,
|
|
208
|
+
interval,
|
|
209
|
+
from,
|
|
210
|
+
to,
|
|
211
|
+
fast,
|
|
212
|
+
slow,
|
|
213
|
+
signal,
|
|
214
|
+
});
|
|
215
|
+
return formatMACDData(macdResult);
|
|
392
216
|
}
|
|
393
|
-
catch (
|
|
394
|
-
|
|
217
|
+
catch (e) {
|
|
218
|
+
if (e instanceof InsufficientDataError) {
|
|
219
|
+
return `Insufficient data: got ${e.got} data points but need at least ${e.required} for MACD(${fast},${slow},${signal}).`;
|
|
220
|
+
}
|
|
221
|
+
return formatToolError(e, 'calculating MACD');
|
|
395
222
|
}
|
|
396
223
|
},
|
|
397
224
|
});
|
|
@@ -407,79 +234,28 @@ export const getBollingerTool = tool({
|
|
|
407
234
|
.max(100)
|
|
408
235
|
.optional()
|
|
409
236
|
.describe('SMA period for the middle band. Defaults to 20.'),
|
|
410
|
-
stdDev: z
|
|
411
|
-
.number()
|
|
412
|
-
.min(0.5)
|
|
413
|
-
.max(5)
|
|
414
|
-
.optional()
|
|
415
|
-
.describe('Standard deviation multiplier for bands. Defaults to 2.'),
|
|
416
237
|
interval: z
|
|
417
238
|
.enum(['daily', 'hourly'])
|
|
418
239
|
.optional()
|
|
419
240
|
.describe('Data interval: "daily" or "hourly". Defaults to "daily".'),
|
|
420
241
|
}),
|
|
421
|
-
execute: async ({ id, period,
|
|
242
|
+
execute: async ({ id, period, from, to, interval }) => {
|
|
243
|
+
const effectivePeriod = period ?? 20;
|
|
422
244
|
try {
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
}
|
|
432
|
-
const bbData = computeBollingerBands(ohlcData, effectivePeriod, effectiveStdDev);
|
|
433
|
-
if (bbData.length === 0) {
|
|
434
|
-
return `Could not compute Bollinger Bands(${effectivePeriod},${effectiveStdDev}) for ${id}.`;
|
|
435
|
-
}
|
|
436
|
-
// Filter results to original date range
|
|
437
|
-
const originalFromTime = new Date(from).getTime();
|
|
438
|
-
const filteredData = bbData.filter((point) => point.timestamp >= originalFromTime);
|
|
439
|
-
if (filteredData.length === 0) {
|
|
440
|
-
return `No Bollinger Bands(${effectivePeriod},${effectiveStdDev}) data available within the requested date range for ${id}.`;
|
|
441
|
-
}
|
|
442
|
-
const lines = [
|
|
443
|
-
`Bollinger Bands(${effectivePeriod},${effectiveStdDev}) for ${id} (${effectiveInterval}, ${filteredData.length} values):`,
|
|
444
|
-
'',
|
|
445
|
-
'Date | Upper | Middle | Lower',
|
|
446
|
-
'--- | --- | --- | ---',
|
|
447
|
-
];
|
|
448
|
-
for (const point of filteredData) {
|
|
449
|
-
const date = new Date(point.timestamp).toISOString().split('T')[0];
|
|
450
|
-
lines.push(`${date} | $${point.upper.toFixed(2)} | $${point.middle.toFixed(2)} | $${point.lower.toFixed(2)}`);
|
|
451
|
-
}
|
|
452
|
-
const latest = filteredData[filteredData.length - 1];
|
|
453
|
-
const latestPrice = ohlcData[ohlcData.length - 1][4];
|
|
454
|
-
const bandwidth = ((latest.upper - latest.lower) / latest.middle) * 100;
|
|
455
|
-
let position = '';
|
|
456
|
-
const percentB = (latestPrice - latest.lower) / (latest.upper - latest.lower);
|
|
457
|
-
if (latestPrice > latest.upper) {
|
|
458
|
-
position = 'ABOVE upper band (potential overbought)';
|
|
459
|
-
}
|
|
460
|
-
else if (latestPrice < latest.lower) {
|
|
461
|
-
position = 'BELOW lower band (potential oversold)';
|
|
462
|
-
}
|
|
463
|
-
else if (percentB > 0.8) {
|
|
464
|
-
position = 'near upper band (approaching overbought)';
|
|
465
|
-
}
|
|
466
|
-
else if (percentB < 0.2) {
|
|
467
|
-
position = 'near lower band (approaching oversold)';
|
|
468
|
-
}
|
|
469
|
-
else {
|
|
470
|
-
position = 'within bands (neutral zone)';
|
|
471
|
-
}
|
|
472
|
-
lines.push('');
|
|
473
|
-
lines.push(`Current price: $${latestPrice.toFixed(2)}`);
|
|
474
|
-
lines.push(`Bands: Upper $${latest.upper.toFixed(2)} | Middle $${latest.middle.toFixed(2)} | Lower $${latest.lower.toFixed(2)}`);
|
|
475
|
-
lines.push(`Bandwidth: ${bandwidth.toFixed(2)}%`);
|
|
476
|
-
lines.push(`%B (position within bands): ${(percentB * 100).toFixed(1)}%`);
|
|
477
|
-
lines.push(`Price position: ${position}`);
|
|
478
|
-
const output = lines.join('\n');
|
|
479
|
-
return output;
|
|
245
|
+
const bbResult = await getBollingerBands({
|
|
246
|
+
project: id,
|
|
247
|
+
interval,
|
|
248
|
+
from,
|
|
249
|
+
to,
|
|
250
|
+
period: effectivePeriod,
|
|
251
|
+
});
|
|
252
|
+
return formatBollingerData(bbResult);
|
|
480
253
|
}
|
|
481
|
-
catch (
|
|
482
|
-
|
|
254
|
+
catch (e) {
|
|
255
|
+
if (e instanceof InsufficientDataError) {
|
|
256
|
+
return `Insufficient data: got ${e.got} data points but need at least ${e.required} for Bollinger Bands(${effectivePeriod}).`;
|
|
257
|
+
}
|
|
258
|
+
return formatToolError(e, 'calculating Bollinger Bands');
|
|
483
259
|
}
|
|
484
260
|
},
|
|
485
261
|
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { truncateTimeseries, truncationLabel } from '../../utils.js';
|
|
2
|
+
export function formatOhlcData(data) {
|
|
3
|
+
const truncated = truncateTimeseries(data, {
|
|
4
|
+
maxDisplay: 30,
|
|
5
|
+
headCount: 10,
|
|
6
|
+
tailCount: 10,
|
|
7
|
+
});
|
|
8
|
+
const lines = ['Date | Open | High | Low | Close'];
|
|
9
|
+
for (const point of truncated) {
|
|
10
|
+
if (point === null) {
|
|
11
|
+
lines.push(truncationLabel(data.length, 20));
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
const iso = new Date(point[0]).toISOString();
|
|
15
|
+
const date = iso.slice(0, 13) + ':00';
|
|
16
|
+
const open = point[1].toFixed(2);
|
|
17
|
+
const high = point[2].toFixed(2);
|
|
18
|
+
const low = point[3].toFixed(2);
|
|
19
|
+
const close = point[4].toFixed(2);
|
|
20
|
+
lines.push(`${date} | $${open} | $${high} | $${low} | $${close}`);
|
|
21
|
+
}
|
|
22
|
+
return lines.join('\n');
|
|
23
|
+
}
|
|
24
|
+
function formatDate(isoTimestamp) {
|
|
25
|
+
const iso = new Date(isoTimestamp).toISOString();
|
|
26
|
+
const date = iso.slice(0, 13) + ':00';
|
|
27
|
+
return date;
|
|
28
|
+
}
|
|
29
|
+
function formatNum(value) {
|
|
30
|
+
const formatted = `$${value.toFixed(2)}`;
|
|
31
|
+
return formatted;
|
|
32
|
+
}
|
|
33
|
+
export function formatIndicatorData(data) {
|
|
34
|
+
const truncated = truncateTimeseries(data);
|
|
35
|
+
const lines = ['Date | Value', '--- | ---'];
|
|
36
|
+
for (const point of truncated) {
|
|
37
|
+
if (point === null) {
|
|
38
|
+
lines.push(truncationLabel(data.length, 15));
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
lines.push(`${formatDate(point.timestamp)} | ${formatNum(point.value)}`);
|
|
42
|
+
}
|
|
43
|
+
const result = lines.join('\n');
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
export function formatMACDData(data) {
|
|
47
|
+
const truncated = truncateTimeseries(data);
|
|
48
|
+
const lines = ['Date | MACD | Signal | Histogram'];
|
|
49
|
+
for (const point of truncated) {
|
|
50
|
+
if (point === null) {
|
|
51
|
+
lines.push(truncationLabel(data.length, 15));
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
lines.push(`${formatDate(point.timestamp)} | ${formatNum(point.macd)} | ${formatNum(point.signal)} | ${formatNum(point.histogram)}`);
|
|
55
|
+
}
|
|
56
|
+
const result = lines.join('\n');
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
export function formatBollingerData(data) {
|
|
60
|
+
const truncated = truncateTimeseries(data);
|
|
61
|
+
const lines = ['Date | Upper | Middle | Lower'];
|
|
62
|
+
for (const point of truncated) {
|
|
63
|
+
if (point === null) {
|
|
64
|
+
lines.push(truncationLabel(data.length, 15));
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
lines.push(`${formatDate(point.timestamp)} | ${formatNum(point.upper)} | ${formatNum(point.middle)} | ${formatNum(point.lower)}`);
|
|
68
|
+
}
|
|
69
|
+
const result = lines.join('\n');
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { tool } from 'ai';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
import { formatPeriodChange, formatToolError, signPrefix, truncateTimeseries, truncationLabel, } from '../../utils.js';
|
|
3
4
|
import { getMindshareClient, } from './client.js';
|
|
4
|
-
import { formatToolError, signPrefix, truncateTimeseries, truncationLabel, formatPeriodChange, } from '../formatting.js';
|
|
5
5
|
const timeframeSchema = z
|
|
6
6
|
.enum(['30m', '24h', '3D', '7D', '1M', '3M', 'YTD'])
|
|
7
7
|
.optional()
|
|
@@ -41,3 +41,47 @@ export function stripCodeFences(text) {
|
|
|
41
41
|
}
|
|
42
42
|
return trimmed;
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Truncate a timeseries array for display, keeping head and tail with a `null` gap in between.
|
|
46
|
+
* Returns the original array unchanged if it fits within `maxDisplay`.
|
|
47
|
+
*/
|
|
48
|
+
export function truncateTimeseries(data, { maxDisplay = 20, headCount = 5, tailCount = 10, } = {}) {
|
|
49
|
+
if (data.length <= maxDisplay) {
|
|
50
|
+
return data;
|
|
51
|
+
}
|
|
52
|
+
const truncated = [...data.slice(0, headCount), null, ...data.slice(-tailCount)];
|
|
53
|
+
return truncated;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Label for the truncation gap (e.g. `"... (42 more points) ..."`).
|
|
57
|
+
*/
|
|
58
|
+
export function truncationLabel(totalLength, shownCount) {
|
|
59
|
+
const hidden = totalLength - shownCount;
|
|
60
|
+
const label = `... (${hidden} more) ...`;
|
|
61
|
+
return label;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Standardised error message for tool catch blocks.
|
|
65
|
+
*/
|
|
66
|
+
export function formatToolError(err, context) {
|
|
67
|
+
const message = extractErrorMessage(err);
|
|
68
|
+
const result = `Error ${context}: ${message}`;
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Returns '+' for non-negative values, '' for negative (the minus sign is already present).
|
|
73
|
+
*/
|
|
74
|
+
export function signPrefix(value) {
|
|
75
|
+
const prefix = value >= 0 ? '+' : '';
|
|
76
|
+
return prefix;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Format a period-change percentage from first → last value.
|
|
80
|
+
* Returns e.g. `"Period change: +12.34%"`.
|
|
81
|
+
*/
|
|
82
|
+
export function formatPeriodChange(firstValue, lastValue) {
|
|
83
|
+
const changePercent = ((lastValue - firstValue) / firstValue) * 100;
|
|
84
|
+
const sign = signPrefix(changePercent);
|
|
85
|
+
const result = `Period change: ${sign}${changePercent.toFixed(2)}%`;
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
@@ -63,6 +63,9 @@ export async function loadAgentConfig(_agentDir) {
|
|
|
63
63
|
if (!avatarUrl) {
|
|
64
64
|
throw new Error('Missing avatarUrl');
|
|
65
65
|
}
|
|
66
|
+
if (!config.apiKey) {
|
|
67
|
+
throw new Error('Missing api key');
|
|
68
|
+
}
|
|
66
69
|
const bioRaw = extractField(soulContent, /^## Bio\s*\n+(.+)$/m);
|
|
67
70
|
const bio = bioRaw ?? null;
|
|
68
71
|
const sentimentRaw = extractField(strategyContent, /^-\s+Bias:\s+(.+)$/m);
|
|
@@ -79,6 +82,7 @@ export async function loadAgentConfig(_agentDir) {
|
|
|
79
82
|
name,
|
|
80
83
|
bio,
|
|
81
84
|
dir: agentDir,
|
|
85
|
+
apiKey: config.apiKey,
|
|
82
86
|
provider,
|
|
83
87
|
avatarUrl,
|
|
84
88
|
soulContent,
|
|
@@ -13,12 +13,7 @@ vi.mock('./ai-providers.js', () => ({
|
|
|
13
13
|
{ label: 'Anthropic', package: '@ai-sdk/anthropic', envVar: 'ANTHROPIC_API_KEY' },
|
|
14
14
|
],
|
|
15
15
|
}));
|
|
16
|
-
vi.mock('@zhive/sdk', () => ({
|
|
17
|
-
loadConfig: vi.fn(),
|
|
18
|
-
}));
|
|
19
|
-
import { loadConfig } from '@zhive/sdk';
|
|
20
16
|
import { findAgentByName, scanAgents } from './agent.js';
|
|
21
|
-
const mockLoadConfig = loadConfig;
|
|
22
17
|
describe('scanAgents', () => {
|
|
23
18
|
beforeEach(() => {
|
|
24
19
|
vi.clearAllMocks();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export class InsufficientDataError extends Error {
|
|
2
|
+
constructor(required, got) {
|
|
3
|
+
super('INSUFFICIENT_DATA');
|
|
4
|
+
this.required = required;
|
|
5
|
+
this.got = got;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export class PriceUnavailableError extends Error {
|
|
9
|
+
constructor(project) {
|
|
10
|
+
super(`Price of "${project}" is not available`);
|
|
11
|
+
}
|
|
12
|
+
}
|