@zhive/cli 0.6.7 → 0.6.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/CLAUDE.md +7 -0
  2. package/dist/backtest/CLAUDE.md +7 -0
  3. package/dist/cli.js +20 -0
  4. package/dist/commands/agent/commands/profile.js +3 -9
  5. package/dist/commands/create/commands/index.js +2 -5
  6. package/dist/commands/create/generate.js +18 -22
  7. package/dist/commands/create/presets.js +613 -0
  8. package/dist/commands/create/ui/CreateApp.js +2 -2
  9. package/dist/commands/create/ui/steps/ScaffoldStep.js +17 -2
  10. package/dist/commands/indicator/commands/bollinger.js +37 -0
  11. package/dist/commands/indicator/commands/ema.js +37 -0
  12. package/dist/commands/indicator/commands/index.js +14 -0
  13. package/dist/commands/indicator/commands/macd.js +51 -0
  14. package/dist/commands/indicator/commands/rsi.js +37 -0
  15. package/dist/commands/indicator/commands/sma.js +37 -0
  16. package/dist/commands/market/commands/index.js +5 -0
  17. package/dist/commands/market/commands/price.js +25 -0
  18. package/dist/commands/shared/utils.js +12 -0
  19. package/dist/commands/start/ui/AsciiTicker.js +81 -0
  20. package/dist/index.js +4 -0
  21. package/dist/services/agent/analysis.js +160 -0
  22. package/dist/services/agent/config.js +75 -0
  23. package/dist/services/agent/env.js +30 -0
  24. package/dist/services/agent/helpers/model.js +92 -0
  25. package/dist/services/agent/helpers.js +22 -0
  26. package/dist/services/agent/prompts/chat-prompt.js +65 -0
  27. package/dist/services/agent/prompts/memory-prompt.js +45 -0
  28. package/dist/services/agent/prompts/prompt.js +379 -0
  29. package/dist/services/agent/skills/index.js +2 -0
  30. package/dist/services/agent/skills/skill-parser.js +149 -0
  31. package/dist/services/agent/skills/types.js +1 -0
  32. package/dist/services/agent/tools/edit-section.js +59 -0
  33. package/dist/services/agent/tools/fetch-rules.js +21 -0
  34. package/dist/services/agent/tools/index.js +76 -0
  35. package/dist/services/agent/tools/market/client.js +41 -0
  36. package/dist/services/agent/tools/market/index.js +3 -0
  37. package/dist/services/agent/tools/market/tools.js +518 -0
  38. package/dist/services/agent/tools/mindshare/client.js +124 -0
  39. package/dist/services/agent/tools/mindshare/index.js +3 -0
  40. package/dist/services/agent/tools/mindshare/tools.js +563 -0
  41. package/dist/services/agent/tools/read-skill-tool.js +30 -0
  42. package/dist/services/agent/tools/ta/index.js +1 -0
  43. package/dist/services/agent/tools/ta/indicators.js +201 -0
  44. package/dist/services/agent/types.js +1 -0
  45. package/dist/services/ai-providers.js +66 -0
  46. package/dist/services/config/agent.js +110 -0
  47. package/dist/services/config/config.js +22 -0
  48. package/dist/services/config/constant.js +8 -0
  49. package/dist/shared/agent/agent-runtime.js +144 -0
  50. package/dist/shared/agent/analysis.js +2 -12
  51. package/dist/shared/agent/cache.js +10 -0
  52. package/dist/shared/agent/config.js +75 -0
  53. package/dist/shared/agent/env.js +30 -0
  54. package/dist/shared/agent/handler.js +3 -9
  55. package/dist/shared/agent/helpers/model.js +92 -0
  56. package/dist/shared/agent/prompts/megathread.js +0 -8
  57. package/dist/shared/agent/tools/execute-skill-tool.js +2 -1
  58. package/dist/shared/agent/tools/formatting.js +0 -19
  59. package/dist/shared/agent/tools/market/client.js +3 -3
  60. package/dist/shared/agent/tools/market/tools.js +88 -312
  61. package/dist/shared/agent/tools/market/utils.js +71 -0
  62. package/dist/shared/agent/tools/mindshare/tools.js +1 -1
  63. package/dist/shared/agent/tools/ta/index.js +3 -1
  64. package/dist/shared/agent/types.js +1 -0
  65. package/dist/shared/agent/utils.js +44 -0
  66. package/dist/shared/ai-providers.js +66 -0
  67. package/dist/shared/ta/error.js +12 -0
  68. package/dist/shared/ta/service.js +93 -0
  69. package/dist/shared/ta/utils.js +16 -0
  70. package/package.json +3 -2
@@ -1,24 +1,9 @@
1
1
  import { tool } from 'ai';
2
2
  import { z } from 'zod';
3
- import { getMarketClient } from './client.js';
4
- import { computeSMA, computeEMA, computeRSI, computeMACD, computeBollingerBands, } from '../ta/indicators.js';
5
- import { formatToolError, signPrefix, truncateTimeseries, truncationLabel } from '../formatting.js';
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 effectiveTimestamp = timestamp ?? new Date().toISOString();
44
- const client = getMarketClient();
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
- const price = priceData.price;
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 client.getOHLC(id, from, to, effectiveInterval);
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
- const lines = [
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 client = getMarketClient();
134
- const effectiveInterval = interval ?? 'daily';
135
- const adjustedFrom = adjustFromDate(from, period, effectiveInterval);
136
- const ohlcData = await client.getOHLC(id, adjustedFrom, to, effectiveInterval);
137
- if (ohlcData.length < period) {
138
- return `Insufficient data: got ${ohlcData.length} data points but need at least ${period} for SMA-${period}.`;
139
- }
140
- const smaData = computeSMA(ohlcData, period);
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 (err) {
168
- return formatToolError(err, 'calculating SMA');
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 client = getMarketClient();
191
- const effectiveInterval = interval ?? 'daily';
192
- const adjustedFrom = adjustFromDate(from, period, effectiveInterval);
193
- const ohlcData = await client.getOHLC(id, adjustedFrom, to, effectiveInterval);
194
- if (ohlcData.length < period) {
195
- return `Insufficient data: got ${ohlcData.length} data points but need at least ${period} for EMA-${period}.`;
196
- }
197
- const emaData = computeEMA(ohlcData, period);
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 (err) {
225
- return formatToolError(err, 'calculating EMA');
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 effectivePeriod = period ?? 14;
243
- const minRequired = effectivePeriod + 1;
244
- const client = getMarketClient();
245
- const effectiveInterval = interval ?? 'daily';
246
- const adjustedFrom = adjustFromDate(from, minRequired, effectiveInterval);
247
- const ohlcData = await client.getOHLC(id, adjustedFrom, to, effectiveInterval);
248
- if (ohlcData.length < minRequired) {
249
- return `Insufficient data: got ${ohlcData.length} data points but need at least ${minRequired} for RSI-${effectivePeriod}.`;
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 (err) {
300
- return formatToolError(err, 'calculating RSI');
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 fast = fastPeriod ?? 12;
338
- const slow = slowPeriod ?? 26;
339
- const signal = signalPeriod ?? 9;
340
- const minRequired = slow + signal;
341
- const client = getMarketClient();
342
- const effectiveInterval = interval ?? 'daily';
343
- const adjustedFrom = adjustFromDate(from, minRequired, effectiveInterval);
344
- const ohlcData = await client.getOHLC(id, adjustedFrom, to, effectiveInterval);
345
- if (ohlcData.length < minRequired) {
346
- return `Insufficient data: got ${ohlcData.length} data points but need at least ${minRequired} for MACD(${fast},${slow},${signal}).`;
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 (err) {
394
- return formatToolError(err, 'calculating MACD');
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, stdDev, from, to, interval }) => {
242
+ execute: async ({ id, period, from, to, interval }) => {
243
+ const effectivePeriod = period ?? 20;
422
244
  try {
423
- const effectivePeriod = period ?? 20;
424
- const effectiveStdDev = stdDev ?? 2;
425
- const client = getMarketClient();
426
- const effectiveInterval = interval ?? 'daily';
427
- const adjustedFrom = adjustFromDate(from, effectivePeriod, effectiveInterval);
428
- const ohlcData = await client.getOHLC(id, adjustedFrom, to, effectiveInterval);
429
- if (ohlcData.length < effectivePeriod) {
430
- return `Insufficient data: got ${ohlcData.length} data points but need at least ${effectivePeriod} for Bollinger Bands(${effectivePeriod},${effectiveStdDev}).`;
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 (err) {
482
- return formatToolError(err, 'calculating Bollinger Bands');
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()
@@ -1 +1,3 @@
1
- export { computeSMA, computeEMA, computeRSI, computeMACD, computeBollingerBands } from './indicators.js';
1
+ export {};
2
+ // Indicator implementations have been moved to shared/ta/service.ts
3
+ // This module is intentionally empty.
@@ -0,0 +1 @@
1
+ export {};
@@ -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
+ }