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..2d125f4d --- /dev/null +++ b/src/ai/generic/indicator.tsx @@ -0,0 +1,84 @@ +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.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..b98f26cf --- /dev/null +++ b/src/server/actions/indicator.ts @@ -0,0 +1,104 @@ +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 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']), + 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[] { + // [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); +} + +function calculateIndicator(indicator: string, closingValues: number[], parameters: IndicatorParameters): number { + switch (indicator) { + case 'rsi': + 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': + 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': + 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': + const macdResult = macd(closingValues, { + fast: parameters.fast || DEFAULT_MACD_FAST, + slow: parameters.slow || DEFAULT_MACD_SLOW, + signal: parameters.signal || DEFAULT_MACD_SIGNAL, + }); + 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'); + } +} + +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'); + const closingValues = getClosingValues(olhcvList, fromTimestamp); + + if (closingValues.length === 0) { + return { success: false, error: 'No closing values found' }; + } + + const result = calculateIndicator(indicator, closingValues, parameters); + return { success: true, data: result }; + } catch (error) { + return { success: false, error: error instanceof Error ? error.message : 'Unexpected error' }; + } + }); +