From 84794ab8f8fa4810e4d64fc92f64ce3f6c8b23c8 Mon Sep 17 00:00:00 2001 From: narasimha-1511 Date: Wed, 19 Feb 2025 01:02:46 +0530 Subject: [PATCH 1/4] feat: added chart Indicator tool --- package.json | 1 + src/ai/generic/indicator.tsx | 86 +++++++++++++++++++++++++++++++++ src/server/actions/chart.ts | 20 ++++---- src/server/actions/charts.ts | 64 ------------------------ src/server/actions/indicator.ts | 71 +++++++++++++++++++++++++++ 5 files changed, 168 insertions(+), 74 deletions(-) create mode 100644 src/ai/generic/indicator.tsx delete mode 100644 src/server/actions/charts.ts create mode 100644 src/server/actions/indicator.ts diff --git a/package.json b/package.json index 81cca357..7943206d 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "date-fns": "^4.1.0", "framer-motion": "^11.18.1", "helius-sdk": "^1.4.1", + "indicatorts": "^2.2.1", "keyv": "4.5.4", "lodash": "^4.17.21", "lucide-react": "^0.468.0", diff --git a/src/ai/generic/indicator.tsx b/src/ai/generic/indicator.tsx new file mode 100644 index 00000000..6acb4080 --- /dev/null +++ b/src/ai/generic/indicator.tsx @@ -0,0 +1,86 @@ +import { checkIndicatorsAction } from "@/server/actions/indicator"; +import { z } from 'zod'; + +// Define the schema for the tool's parameters +const indicatorToolParameters = z.object({ + indicator: z.enum(['rsi', 'sma', 'bb', 'macd']).describe('The indicator to check'), + parameters: z.object({ + period: z.number().optional().describe('The period for the indicator'), + fast: z.number().optional(), + slow: z.number().optional(), + signal: z.number().optional(), + }), + contractAddress: z.string().describe('The contract address of the token to check'), + fromTimestamp: z.number().optional().describe('The timestamp to start checking from'), +}); + +// Define the tool +export const indicatorTools = { + checkIndicators: { + displayName: '📈 Indicator Tool', + description: 'Check financial indicators like RSI, SMA, BB, and MACD for a given contract address.', + parameters: indicatorToolParameters, + execute: async (input: z.infer) => { + try { + // Call the existing checkIndicatorsAction function + const result = await checkIndicatorsAction(input); + + if(!result?.data) { + return { + success: false, + error: 'Unable to fetch indicator data.', + }; + } + + if (result?.data?.success) { + + return { + success: true, + data: result.data, + suppressFollowUp: true, + }; + + } else { + return { + success: false, + error: result.data.error, + }; + } + } catch (error) { + return { + success: false, + error: 'Unexpected error during indicator check', + }; + } + }, + render: (result: unknown) => { + const typedResult = result as { + success: boolean; + data?: number; + error?: string; + }; + + if (!typedResult.success) { + return ( +
+
+

+ {typedResult.error || 'Unable to fetch indicator data.'} +

+
+
+ ); + } + + return ( +
+
+

Indicator Result

+ +

{typedResult.data}

+
+
+ ); + }, + }, +}; \ No newline at end of file diff --git a/src/server/actions/chart.ts b/src/server/actions/chart.ts index aa5226e1..9f7af8af 100644 --- a/src/server/actions/chart.ts +++ b/src/server/actions/chart.ts @@ -98,7 +98,7 @@ export async function getPriceHistoryFromCG( return parsed.prices.map(([time, value]) => ({ time, value })); } -async function getTokenPools( +export async function getTokenPools( contractAddress: string, network: string = 'solana', ): Promise { @@ -121,13 +121,13 @@ async function getTokenPools( return topPoolId; } -async function getDexOhlcv( +export async function getDexOhlcv( poolId: string, network: string = 'solana', timeFrame: TIMEFRAME = TIMEFRAME.MINUTES, aggregator?: string, beforeTimestamp?: number, -): Promise<{ time: number; value: number }[]> { +): Promise { if (!API_KEY) throw new Error('API key not found'); const path = mapTimeframeToDexPath(timeFrame); const agg = validateAggregator(timeFrame, aggregator); @@ -143,12 +143,8 @@ async function getDexOhlcv( const parsed = dexOhlcvApiResponseSchema.parse(data); const ohlcvList = parsed.data.attributes.ohlcv_list; - const reversedOhlcv = ohlcvList.map(([timestamp, open, high, low, close]) => { - const price = close ?? open ?? 0; - return { time: timestamp * 1000, value: price }; - }); - reversedOhlcv.reverse(); - return reversedOhlcv; + ohlcvList.reverse(); + return ohlcvList; } export async function getDexPriceHistory( @@ -159,13 +155,17 @@ export async function getDexPriceHistory( beforeTimestamp?: number, ): Promise<{ time: number; value: number }[]> { const topPoolId = await getTokenPools(contractAddress, network); - return getDexOhlcv( + const ohlcvList = await getDexOhlcv( topPoolId, network, timeFrame, aggregator, beforeTimestamp, ); + return ohlcvList.map(([timestamp, open, high, low, close, volume]) => { + const price = close ?? open ?? 0; + return { time: timestamp * 1000, value: price }; + }); } export async function getPriceHistory( diff --git a/src/server/actions/charts.ts b/src/server/actions/charts.ts deleted file mode 100644 index 6c5abe42..00000000 --- a/src/server/actions/charts.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { z } from 'zod'; - -const API_KEY = process.env.CG_API_KEY; -const BASE_URL = process.env.CG_BASE_URL || 'https://api.coingecko.com/api/v3'; - -const tokenSchema = z.object({ - id: z.string(), -}); - -const priceHistorySchema = z.object({ - prices: z.array(z.tuple([z.number(), z.number()])), -}); - -export const getTokenId = async (contractAddress: string): Promise => { - if (!API_KEY) { - throw new Error('API key not found'); - } - const url = `${BASE_URL}/coins/solana/contract/${contractAddress}`; - const options = { - method: 'GET', - headers: { - accept: 'application/json', - 'x-cg-demo-api-key': API_KEY, - }, - }; - - const response = await fetch(url, options); - if (!response.ok) { - throw new Error('Failed to fetch token ID'); - } - - const data = await response.json(); - const parsed = tokenSchema.parse(data); - return parsed.id; -}; - -export const getPriceHistory = async ( - tokenId: string, - days: number = 7, -): Promise<{ time: string; value: number }[]> => { - if (!API_KEY) { - throw new Error('API key not found'); - } - const url = `${BASE_URL}/coins/${tokenId}/market_chart?vs_currency=usd&days=${days}&precision=18`; - const options = { - method: 'GET', - headers: { - accept: 'application/json', - 'x-cg-demo-api-key': API_KEY, - }, - }; - - const response = await fetch(url, options); - if (!response.ok) { - throw new Error('Failed to fetch price history'); - } - - const data = await response.json(); - const parsed = priceHistorySchema.parse(data); - return parsed.prices.map(([time, value]) => ({ - time: new Date(time).toLocaleString(), - value, - })); -}; diff --git a/src/server/actions/indicator.ts b/src/server/actions/indicator.ts new file mode 100644 index 00000000..5bef0ee8 --- /dev/null +++ b/src/server/actions/indicator.ts @@ -0,0 +1,71 @@ +import { actionClient, ActionResponse } from "@/lib/safe-action"; +import { z } from 'zod'; +import { getDexOhlcv, getTokenPools } from '@/server/actions/chart'; +import { rsi, sma, bb , macd } from 'indicatorts'; +import { TIMEFRAME } from "@/types/chart"; + +const indicatorSchema = z.object({ + indicator: z.enum(['rsi', 'sma', 'bb' , 'macd']), + parameters: z.object({ + period: z.number().optional(), + fast: z.number().optional(), + slow: z.number().optional(), + signal: z.number().optional(), + }), + contractAddress: z.string(), + fromTimestamp: z.number().optional(), +}); + +function getClosingValues(olhcvList: number[][] , fromTimestamp?: number): number[] { + + let closingValues; + + if (fromTimestamp) { + closingValues = olhcvList.filter(([timestamp]) => timestamp >= fromTimestamp).map(([timestamp, open, high, low, close , volume]) => close); + } else { + closingValues = olhcvList.map(([timestamp, open, high, low, close , volume]) => close); + } + + return closingValues; +} + +export const checkIndicatorsAction = actionClient + .schema(indicatorSchema) + .action(async (input): Promise> => { + try { + const { indicator, parameters, contractAddress, fromTimestamp } = input.parsedInput; + + const topPoolId = await getTokenPools(contractAddress , 'solana'); + const olhcvList = await getDexOhlcv(topPoolId, 'solana', TIMEFRAME.MINUTES, '1'); + let result; + + const closingValues = getClosingValues(olhcvList , fromTimestamp); + + + switch (indicator) { + case 'rsi': + const rsiResult = rsi(closingValues, { period: parameters.period || 14 }); + result = rsiResult[rsiResult.length - 1]; + break; + case 'sma': + const smaResult = sma(closingValues, { period: parameters.period || 14 }); + result = smaResult[smaResult.length - 1]; + break; + case 'bb': + const bbResult = bb(closingValues, { period: parameters.period || 14 }); + result = bbResult.upper[bbResult.upper.length - 1]; + break; + case 'macd': + const macdResult = macd(closingValues, { fast: parameters.fast || 12, slow: parameters.slow || 26, signal: parameters.signal || 9 }); + result = macdResult.macdLine[macdResult.macdLine.length - 1]; + break; + default: + throw new Error('Unsupported indicator'); + } + + return { success: true, data: result }; + } catch (error) { + return { success: false, error: error instanceof Error ? error.message : 'Unexpected error' }; + } + }); + From 791c68b77cb012bf5cc456289afea065da8f37e8 Mon Sep 17 00:00:00 2001 From: narasimha-1511 Date: Wed, 19 Feb 2025 09:37:23 +0530 Subject: [PATCH 2/4] fix: logic flow --- src/ai/generic/indicator.tsx | 4 +--- src/server/actions/indicator.ts | 3 +++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ai/generic/indicator.tsx b/src/ai/generic/indicator.tsx index 6acb4080..2d125f4d 100644 --- a/src/ai/generic/indicator.tsx +++ b/src/ai/generic/indicator.tsx @@ -33,13 +33,11 @@ export const indicatorTools = { } if (result?.data?.success) { - return { success: true, - data: result.data, + data: result.data.data, suppressFollowUp: true, }; - } else { return { success: false, diff --git a/src/server/actions/indicator.ts b/src/server/actions/indicator.ts index 5bef0ee8..61a6fbda 100644 --- a/src/server/actions/indicator.ts +++ b/src/server/actions/indicator.ts @@ -41,6 +41,9 @@ export const checkIndicatorsAction = actionClient const closingValues = getClosingValues(olhcvList , fromTimestamp); + if(closingValues.length === 0) { + return { success: false, error: 'No closing values found' }; + } switch (indicator) { case 'rsi': From bf725f2c264781e552dbc94e7ad1c5ecf7a7da65 Mon Sep 17 00:00:00 2001 From: narasimha-1511 Date: Wed, 19 Feb 2025 09:51:07 +0530 Subject: [PATCH 3/4] fix: refactoring code --- src/server/actions/indicator.ts | 86 ++++++++++++++++++--------------- 1 file changed, 48 insertions(+), 38 deletions(-) diff --git a/src/server/actions/indicator.ts b/src/server/actions/indicator.ts index 61a6fbda..7a6134b7 100644 --- a/src/server/actions/indicator.ts +++ b/src/server/actions/indicator.ts @@ -4,8 +4,27 @@ import { getDexOhlcv, getTokenPools } from '@/server/actions/chart'; import { rsi, sma, bb , macd } from 'indicatorts'; import { TIMEFRAME } from "@/types/chart"; +const DEFAULT_PERIOD = 14; +const DEFAULT_MACD_FAST = 12; +const DEFAULT_MACD_SLOW = 26; +const DEFAULT_MACD_SIGNAL = 9; + +interface IndicatorParameters { + period?: number; + fast?: number; + slow?: number; + signal?: number; +} + +interface IndicatorInput { + indicator: 'rsi' | 'sma' | 'bb' | 'macd'; + parameters: IndicatorParameters; + contractAddress: string; + fromTimestamp?: number; +} + const indicatorSchema = z.object({ - indicator: z.enum(['rsi', 'sma', 'bb' , 'macd']), + indicator: z.enum(['rsi', 'sma', 'bb', 'macd']), parameters: z.object({ period: z.number().optional(), fast: z.number().optional(), @@ -16,56 +35,47 @@ const indicatorSchema = z.object({ fromTimestamp: z.number().optional(), }); -function getClosingValues(olhcvList: number[][] , fromTimestamp?: number): number[] { - - let closingValues; +function getClosingValues(olhcvList: number[][], fromTimestamp?: number): number[] { + // [timestamp, open, high, low, close , volume] // this is the format of the olhcvList + return fromTimestamp + ? olhcvList.filter(([timestamp]) => timestamp >= fromTimestamp).map(([, , , , close]) => close) + : olhcvList.map(([, , , , close]) => close); +} - if (fromTimestamp) { - closingValues = olhcvList.filter(([timestamp]) => timestamp >= fromTimestamp).map(([timestamp, open, high, low, close , volume]) => close); - } else { - closingValues = olhcvList.map(([timestamp, open, high, low, close , volume]) => close); +function calculateIndicator(indicator: string, closingValues: number[], parameters: IndicatorParameters): number { + switch (indicator) { + case 'rsi': + return rsi(closingValues, { period: parameters.period || DEFAULT_PERIOD }).pop()!; + case 'sma': + return sma(closingValues, { period: parameters.period || DEFAULT_PERIOD }).pop()!; + case 'bb': + return bb(closingValues, { period: parameters.period || DEFAULT_PERIOD }).upper.pop()!; + case 'macd': + return macd(closingValues, { + fast: parameters.fast || DEFAULT_MACD_FAST, + slow: parameters.slow || DEFAULT_MACD_SLOW, + signal: parameters.signal || DEFAULT_MACD_SIGNAL, + }).macdLine.pop()!; + default: + throw new Error('Unsupported indicator'); } - - return closingValues; } export const checkIndicatorsAction = actionClient .schema(indicatorSchema) .action(async (input): Promise> => { try { - const { indicator, parameters, contractAddress, fromTimestamp } = input.parsedInput; + const { indicator, parameters, contractAddress, fromTimestamp } = input.parsedInput; - const topPoolId = await getTokenPools(contractAddress , 'solana'); + const topPoolId = await getTokenPools(contractAddress, 'solana'); const olhcvList = await getDexOhlcv(topPoolId, 'solana', TIMEFRAME.MINUTES, '1'); - let result; - - const closingValues = getClosingValues(olhcvList , fromTimestamp); + const closingValues = getClosingValues(olhcvList, fromTimestamp); - if(closingValues.length === 0) { + if (closingValues.length === 0) { return { success: false, error: 'No closing values found' }; } - - switch (indicator) { - case 'rsi': - const rsiResult = rsi(closingValues, { period: parameters.period || 14 }); - result = rsiResult[rsiResult.length - 1]; - break; - case 'sma': - const smaResult = sma(closingValues, { period: parameters.period || 14 }); - result = smaResult[smaResult.length - 1]; - break; - case 'bb': - const bbResult = bb(closingValues, { period: parameters.period || 14 }); - result = bbResult.upper[bbResult.upper.length - 1]; - break; - case 'macd': - const macdResult = macd(closingValues, { fast: parameters.fast || 12, slow: parameters.slow || 26, signal: parameters.signal || 9 }); - result = macdResult.macdLine[macdResult.macdLine.length - 1]; - break; - default: - throw new Error('Unsupported indicator'); - } - + + const result = calculateIndicator(indicator, closingValues, parameters); return { success: true, data: result }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Unexpected error' }; From 001ef1c7b0874162c8ff137a4fd2ec31f574d2de Mon Sep 17 00:00:00 2001 From: narasimha-1511 Date: Wed, 19 Feb 2025 10:11:08 +0530 Subject: [PATCH 4/4] fix: refactor --- src/server/actions/indicator.ts | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/server/actions/indicator.ts b/src/server/actions/indicator.ts index 7a6134b7..b98f26cf 100644 --- a/src/server/actions/indicator.ts +++ b/src/server/actions/indicator.ts @@ -45,17 +45,37 @@ function getClosingValues(olhcvList: number[][], fromTimestamp?: number): number function calculateIndicator(indicator: string, closingValues: number[], parameters: IndicatorParameters): number { switch (indicator) { case 'rsi': - return rsi(closingValues, { period: parameters.period || DEFAULT_PERIOD }).pop()!; + const rsiResult = rsi(closingValues, { period: parameters.period || DEFAULT_PERIOD }); + const rsiValue = rsiResult[rsiResult.length - 1]; + if(typeof rsiValue !== 'number') { + throw new Error('Failed to calculate RSI'); + } + return rsiValue; case 'sma': - return sma(closingValues, { period: parameters.period || DEFAULT_PERIOD }).pop()!; + const smaResult = sma(closingValues, { period: parameters.period || DEFAULT_PERIOD }); + const smaValue = smaResult[smaResult.length - 1]; + if(typeof smaValue !== 'number') { + throw new Error('Failed to calculate SMA'); + } + return smaValue; case 'bb': - return bb(closingValues, { period: parameters.period || DEFAULT_PERIOD }).upper.pop()!; + const bbResult = bb(closingValues, { period: parameters.period || DEFAULT_PERIOD }); + const bbValue = bbResult.upper[bbResult.upper.length - 1]; + if(typeof bbValue !== 'number') { + throw new Error('Failed to calculate BB'); + } + return bbValue; case 'macd': - return macd(closingValues, { + const macdResult = macd(closingValues, { fast: parameters.fast || DEFAULT_MACD_FAST, slow: parameters.slow || DEFAULT_MACD_SLOW, signal: parameters.signal || DEFAULT_MACD_SIGNAL, - }).macdLine.pop()!; + }); + const macdValue = macdResult.macdLine[macdResult.macdLine.length - 1]; + if(typeof macdValue !== 'number') { + throw new Error('Failed to calculate MACD'); + } + return macdValue; default: throw new Error('Unsupported indicator'); }