analyzePeriodicReturns
Calculate and analyze daily or monthly returns for cryptocurrency trading accounts over specified periods to assess performance.
Instructions
Analyze daily and monthly returns for a configured account
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| accountName | Yes | Account name defined in the configuration file (e.g., 'bybit_main') | |
| symbol | No | Optional trading symbol (e.g., 'BTC/USDT') to filter trades | |
| period | No | Analysis period: '30d', '90d', '180d', or '1y' | 90d |
| interval | No | Return calculation interval | daily |
Implementation Reference
- src/tools/analysis-tools.ts:284-346 (handler)The async handler function that implements the core logic of analyzePeriodicReturns: fetches trades via CCXT, computes since timestamp based on period, calls calculatePeriodicReturns helper, and returns JSON results or error.async ({ accountName, symbol, period, interval }) => { try { const exchange = ccxtServer.getExchangeInstance(accountName); // fetchMyTrades 메서드가 지원되는지 확인 if (!exchange.has["fetchMyTrades"]) { return { content: [ { type: "text", text: `Account '${accountName}' (Exchange: ${exchange.id}) does not support fetching personal trades for periodic returns analysis`, }, ], isError: true, }; } // 기간에 따른 since 값 계산 const now = Date.now(); let since; switch (period) { case "30d": since = now - 30 * 24 * 60 * 60 * 1000; break; case "90d": since = now - 90 * 24 * 60 * 60 * 1000; break; case "180d": since = now - 180 * 24 * 60 * 60 * 1000; break; case "1y": since = now - 365 * 24 * 60 * 60 * 1000; break; } // 거래 내역 가져오기 const trades = await exchange.fetchMyTrades(symbol, since, undefined); // 기간별 수익률 계산 const returns = calculatePeriodicReturns(trades, interval); return { content: [ { type: "text", text: JSON.stringify(returns, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error analyzing periodic returns for account '${accountName}': ${ (error as Error).message }`, }, ], isError: true, }; } }
- src/tools/analysis-tools.ts:265-283 (schema)Zod input schema defining parameters for the tool: accountName (string), symbol (optional string), period (enum: '30d', '90d', '180d', '1y'), interval (enum: 'daily', 'weekly', 'monthly').{ accountName: z .string() .describe( "Account name defined in the configuration file (e.g., 'bybit_main')" ), symbol: z .string() .optional() .describe("Optional trading symbol (e.g., 'BTC/USDT') to filter trades"), period: z .enum(["30d", "90d", "180d", "1y"]) .default("90d") .describe("Analysis period: '30d', '90d', '180d', or '1y'"), interval: z .enum(["daily", "weekly", "monthly"]) .default("daily") .describe("Return calculation interval"), },
- src/tools/analysis-tools.ts:263-347 (registration)MCP server tool registration: server.tool('analyzePeriodicReturns', description, inputSchema, handlerFn) within registerAnalysisTools function."analyzePeriodicReturns", "Analyze daily and monthly returns for a configured account", { accountName: z .string() .describe( "Account name defined in the configuration file (e.g., 'bybit_main')" ), symbol: z .string() .optional() .describe("Optional trading symbol (e.g., 'BTC/USDT') to filter trades"), period: z .enum(["30d", "90d", "180d", "1y"]) .default("90d") .describe("Analysis period: '30d', '90d', '180d', or '1y'"), interval: z .enum(["daily", "weekly", "monthly"]) .default("daily") .describe("Return calculation interval"), }, async ({ accountName, symbol, period, interval }) => { try { const exchange = ccxtServer.getExchangeInstance(accountName); // fetchMyTrades 메서드가 지원되는지 확인 if (!exchange.has["fetchMyTrades"]) { return { content: [ { type: "text", text: `Account '${accountName}' (Exchange: ${exchange.id}) does not support fetching personal trades for periodic returns analysis`, }, ], isError: true, }; } // 기간에 따른 since 값 계산 const now = Date.now(); let since; switch (period) { case "30d": since = now - 30 * 24 * 60 * 60 * 1000; break; case "90d": since = now - 90 * 24 * 60 * 60 * 1000; break; case "180d": since = now - 180 * 24 * 60 * 60 * 1000; break; case "1y": since = now - 365 * 24 * 60 * 60 * 1000; break; } // 거래 내역 가져오기 const trades = await exchange.fetchMyTrades(symbol, since, undefined); // 기간별 수익률 계산 const returns = calculatePeriodicReturns(trades, interval); return { content: [ { type: "text", text: JSON.stringify(returns, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error analyzing periodic returns for account '${accountName}': ${ (error as Error).message }`, }, ], isError: true, }; } } );
- src/tools/analysis-tools.ts:681-763 (helper)Helper function calculatePeriodicReturns that groups trades by specified interval (daily/weekly/monthly), computes profit per period, and generates statistics like total profit, average, best/worst periods.function calculatePeriodicReturns(trades: any[], interval: string) { if (!trades || trades.length === 0) { return { totalTrades: 0, message: "No trades found for the specified period.", }; } // 거래를 시간순으로 정렬 trades.sort((a, b) => a.timestamp - b.timestamp); // 기간별로 그룹화 const periodicData: Record<string, { profit: number, trades: number }> = {}; trades.forEach(trade => { const date = new Date(trade.timestamp); let key: string; switch(interval) { case 'weekly': // 주차 계산 (ISO 주 - 1부터 53까지) const weekOfYear = getWeekNumber(date); key = `${date.getFullYear()}-W${weekOfYear}`; break; case 'monthly': // 월 (1월은 0) key = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}`; break; case 'daily': default: // 일 (YYYY-MM-DD) key = date.toISOString().split('T')[0]; } if (!periodicData[key]) { periodicData[key] = { profit: 0, trades: 0 }; } // 단순화된 손익 계산 const profit = trade.side === 'buy' ? -(trade.amount * trade.price) : (trade.amount * trade.price); periodicData[key].profit += profit - (trade.fee?.cost || 0); periodicData[key].trades++; }); // 결과 처리 const returns = Object.entries(periodicData).map(([period, data]) => ({ period, profit: data.profit.toFixed(8), trades: data.trades })).sort((a, b) => a.period.localeCompare(b.period)); // 통계 계산 const profitValues = returns.map(r => parseFloat(r.profit)); const totalProfit = profitValues.reduce((sum, profit) => sum + profit, 0); const averagePeriodProfit = profitValues.length > 0 ? totalProfit / profitValues.length : 0; const positiveReturns = profitValues.filter(p => p > 0); const negativeReturns = profitValues.filter(p => p < 0); return { interval, totalPeriods: returns.length, totalProfit: totalProfit.toFixed(8), averagePeriodProfit: averagePeriodProfit.toFixed(8), profitablePeriods: positiveReturns.length, lossPeriods: negativeReturns.length, profitablePeriodRatio: returns.length > 0 ? ((positiveReturns.length / returns.length) * 100).toFixed(2) + '%' : '0%', bestPeriod: profitValues.length > 0 ? returns[profitValues.indexOf(Math.max(...profitValues))] : null, worstPeriod: profitValues.length > 0 ? returns[profitValues.indexOf(Math.min(...profitValues))] : null, periodicReturns: returns }; }
- src/server.ts:375-375 (registration)Top-level registration call: registerAnalysisTools(this.server, this) in CcxtMcpServer.registerTools() method, which triggers the tool registrations including analyzePeriodicReturns.registerAnalysisTools(this.server, this);