393 lines
10 KiB
TypeScript
393 lines
10 KiB
TypeScript
/**
|
|
* Binance Market Data Tools
|
|
*
|
|
* - binance_get_ticker: Get current price and 24h stats
|
|
* - binance_get_orderbook: Get order book depth
|
|
* - binance_get_klines: Get OHLCV candles
|
|
*
|
|
* @version 1.0.0
|
|
* @author Trading Platform Trading Platform
|
|
*/
|
|
|
|
import { z } from 'zod';
|
|
import { getBinanceClient, BinanceTicker, BinanceOrderBook, BinanceKline } from '../services/binance-client';
|
|
|
|
// ==========================================
|
|
// binance_get_ticker
|
|
// ==========================================
|
|
|
|
/**
|
|
* Tool: binance_get_ticker
|
|
* Get current price and 24h statistics for a trading pair
|
|
*/
|
|
export const binanceGetTickerSchema = {
|
|
name: 'binance_get_ticker',
|
|
description: 'Get the current price and 24-hour statistics for a Binance trading pair',
|
|
inputSchema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
symbol: {
|
|
type: 'string',
|
|
description: 'Trading pair symbol (e.g., BTCUSDT, ETHUSDT, BNBUSDT)',
|
|
},
|
|
},
|
|
required: ['symbol'] as string[],
|
|
},
|
|
};
|
|
|
|
export const BinanceGetTickerInputSchema = z.object({
|
|
symbol: z.string().min(1).max(20).transform((s) => s.toUpperCase()),
|
|
});
|
|
|
|
export type BinanceGetTickerInput = z.infer<typeof BinanceGetTickerInputSchema>;
|
|
|
|
export interface BinanceGetTickerResult {
|
|
success: boolean;
|
|
data?: BinanceTicker;
|
|
error?: string;
|
|
}
|
|
|
|
export async function binance_get_ticker(
|
|
params: BinanceGetTickerInput
|
|
): Promise<BinanceGetTickerResult> {
|
|
try {
|
|
const client = getBinanceClient();
|
|
const ticker = await client.getTicker(params.symbol);
|
|
|
|
return {
|
|
success: true,
|
|
data: ticker,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
};
|
|
}
|
|
}
|
|
|
|
export async function handleBinanceGetTicker(
|
|
params: unknown
|
|
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
|
const validatedParams = BinanceGetTickerInputSchema.parse(params);
|
|
const result = await binance_get_ticker(validatedParams);
|
|
|
|
if (result.success && result.data) {
|
|
const d = result.data;
|
|
const changeSymbol = d.change24h >= 0 ? '+' : '';
|
|
|
|
const formattedOutput = `
|
|
Binance Ticker: ${d.symbol}
|
|
${'='.repeat(35)}
|
|
Current Price: $${d.price.toFixed(getPriceDecimals(d.symbol))}
|
|
Bid: $${d.bid.toFixed(getPriceDecimals(d.symbol))}
|
|
Ask: $${d.ask.toFixed(getPriceDecimals(d.symbol))}
|
|
|
|
24h Statistics
|
|
--------------
|
|
High: $${d.high24h.toFixed(getPriceDecimals(d.symbol))}
|
|
Low: $${d.low24h.toFixed(getPriceDecimals(d.symbol))}
|
|
Volume: ${formatVolume(d.volume24h)}
|
|
Change: ${changeSymbol}${d.change24h.toFixed(2)}%
|
|
|
|
Last Update: ${new Date(d.timestamp).toISOString()}
|
|
`.trim();
|
|
|
|
return {
|
|
content: [{ type: 'text', text: formattedOutput }],
|
|
};
|
|
}
|
|
|
|
return {
|
|
content: [{ type: 'text', text: `Error: ${result.error}` }],
|
|
};
|
|
}
|
|
|
|
// ==========================================
|
|
// binance_get_orderbook
|
|
// ==========================================
|
|
|
|
/**
|
|
* Tool: binance_get_orderbook
|
|
* Get order book (bids and asks) with specified depth
|
|
*/
|
|
export const binanceGetOrderbookSchema = {
|
|
name: 'binance_get_orderbook',
|
|
description: 'Get the order book (bids and asks) with the specified depth for a trading pair',
|
|
inputSchema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
symbol: {
|
|
type: 'string',
|
|
description: 'Trading pair symbol (e.g., BTCUSDT)',
|
|
},
|
|
limit: {
|
|
type: 'number',
|
|
description: 'Order book depth (5, 10, 20, 50, or 100). Default: 20',
|
|
},
|
|
},
|
|
required: ['symbol'] as string[],
|
|
},
|
|
};
|
|
|
|
export const BinanceGetOrderbookInputSchema = z.object({
|
|
symbol: z.string().min(1).max(20).transform((s) => s.toUpperCase()),
|
|
limit: z.number().int().min(5).max(100).default(20),
|
|
});
|
|
|
|
export type BinanceGetOrderbookInput = z.infer<typeof BinanceGetOrderbookInputSchema>;
|
|
|
|
export interface BinanceGetOrderbookResult {
|
|
success: boolean;
|
|
data?: BinanceOrderBook;
|
|
error?: string;
|
|
}
|
|
|
|
export async function binance_get_orderbook(
|
|
params: BinanceGetOrderbookInput
|
|
): Promise<BinanceGetOrderbookResult> {
|
|
try {
|
|
const client = getBinanceClient();
|
|
const orderbook = await client.getOrderBook(params.symbol, params.limit);
|
|
|
|
return {
|
|
success: true,
|
|
data: orderbook,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
};
|
|
}
|
|
}
|
|
|
|
export async function handleBinanceGetOrderbook(
|
|
params: unknown
|
|
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
|
const validatedParams = BinanceGetOrderbookInputSchema.parse(params);
|
|
const result = await binance_get_orderbook(validatedParams);
|
|
|
|
if (result.success && result.data) {
|
|
const d = result.data;
|
|
const decimals = getPriceDecimals(d.symbol);
|
|
|
|
// Format top 10 levels
|
|
const topBids = d.bids.slice(0, 10);
|
|
const topAsks = d.asks.slice(0, 10);
|
|
|
|
let bidsStr = topBids
|
|
.map(([price, qty]) => ` $${price.toFixed(decimals)} | ${qty.toFixed(6)}`)
|
|
.join('\n');
|
|
|
|
let asksStr = topAsks
|
|
.map(([price, qty]) => ` $${price.toFixed(decimals)} | ${qty.toFixed(6)}`)
|
|
.join('\n');
|
|
|
|
const formattedOutput = `
|
|
Order Book: ${d.symbol}
|
|
${'='.repeat(35)}
|
|
Spread: $${d.spread.toFixed(decimals)} (${d.spreadPercentage.toFixed(4)}%)
|
|
|
|
Top ${topAsks.length} Asks (Sell Orders)
|
|
${'-'.repeat(25)}
|
|
${asksStr}
|
|
|
|
Top ${topBids.length} Bids (Buy Orders)
|
|
${'-'.repeat(25)}
|
|
${bidsStr}
|
|
|
|
Timestamp: ${new Date(d.timestamp).toISOString()}
|
|
`.trim();
|
|
|
|
return {
|
|
content: [{ type: 'text', text: formattedOutput }],
|
|
};
|
|
}
|
|
|
|
return {
|
|
content: [{ type: 'text', text: `Error: ${result.error}` }],
|
|
};
|
|
}
|
|
|
|
// ==========================================
|
|
// binance_get_klines
|
|
// ==========================================
|
|
|
|
/**
|
|
* Tool: binance_get_klines
|
|
* Get historical OHLCV candles for technical analysis
|
|
*/
|
|
export const binanceGetKlinesSchema = {
|
|
name: 'binance_get_klines',
|
|
description: 'Get historical candlestick (OHLCV) data for technical analysis',
|
|
inputSchema: {
|
|
type: 'object' as const,
|
|
properties: {
|
|
symbol: {
|
|
type: 'string',
|
|
description: 'Trading pair symbol (e.g., BTCUSDT)',
|
|
},
|
|
interval: {
|
|
type: 'string',
|
|
description: 'Candle interval: 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w. Default: 5m',
|
|
enum: ['1m', '5m', '15m', '30m', '1h', '4h', '1d', '1w'],
|
|
},
|
|
limit: {
|
|
type: 'number',
|
|
description: 'Number of candles to retrieve (max 500). Default: 100',
|
|
},
|
|
},
|
|
required: ['symbol'] as string[],
|
|
},
|
|
};
|
|
|
|
export const BinanceGetKlinesInputSchema = z.object({
|
|
symbol: z.string().min(1).max(20).transform((s) => s.toUpperCase()),
|
|
interval: z.enum(['1m', '5m', '15m', '30m', '1h', '4h', '1d', '1w']).default('5m'),
|
|
limit: z.number().int().min(1).max(500).default(100),
|
|
});
|
|
|
|
export type BinanceGetKlinesInput = z.infer<typeof BinanceGetKlinesInputSchema>;
|
|
|
|
export interface BinanceGetKlinesResult {
|
|
success: boolean;
|
|
data?: {
|
|
symbol: string;
|
|
interval: string;
|
|
candles: BinanceKline[];
|
|
count: number;
|
|
};
|
|
error?: string;
|
|
}
|
|
|
|
export async function binance_get_klines(
|
|
params: BinanceGetKlinesInput
|
|
): Promise<BinanceGetKlinesResult> {
|
|
try {
|
|
const client = getBinanceClient();
|
|
const klines = await client.getKlines(params.symbol, params.interval, params.limit);
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
symbol: params.symbol,
|
|
interval: params.interval,
|
|
candles: klines,
|
|
count: klines.length,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
};
|
|
}
|
|
}
|
|
|
|
export async function handleBinanceGetKlines(
|
|
params: unknown
|
|
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
|
const validatedParams = BinanceGetKlinesInputSchema.parse(params);
|
|
const result = await binance_get_klines(validatedParams);
|
|
|
|
if (result.success && result.data) {
|
|
const d = result.data;
|
|
const decimals = getPriceDecimals(d.symbol);
|
|
|
|
// Get last 5 candles for display
|
|
const recentCandles = d.candles.slice(-5);
|
|
|
|
let candlesStr = recentCandles
|
|
.map((c) => {
|
|
const time = new Date(c.timestamp).toISOString().slice(0, 16).replace('T', ' ');
|
|
const direction = c.close >= c.open ? 'UP' : 'DOWN';
|
|
return ` ${time} | O:${c.open.toFixed(decimals)} H:${c.high.toFixed(decimals)} L:${c.low.toFixed(decimals)} C:${c.close.toFixed(decimals)} | V:${formatVolume(c.volume)} | ${direction}`;
|
|
})
|
|
.join('\n');
|
|
|
|
// Calculate basic stats
|
|
const closes = d.candles.map((c) => c.close);
|
|
const high = Math.max(...d.candles.map((c) => c.high));
|
|
const low = Math.min(...d.candles.map((c) => c.low));
|
|
const avgVolume = d.candles.reduce((sum, c) => sum + c.volume, 0) / d.candles.length;
|
|
|
|
const formattedOutput = `
|
|
Klines: ${d.symbol} (${d.interval})
|
|
${'='.repeat(45)}
|
|
Retrieved: ${d.count} candles
|
|
|
|
Period Statistics
|
|
-----------------
|
|
Highest High: $${high.toFixed(decimals)}
|
|
Lowest Low: $${low.toFixed(decimals)}
|
|
Avg Volume: ${formatVolume(avgVolume)}
|
|
|
|
Recent Candles (last 5)
|
|
-----------------------
|
|
${candlesStr}
|
|
|
|
First Candle: ${new Date(d.candles[0].timestamp).toISOString()}
|
|
Last Candle: ${new Date(d.candles[d.candles.length - 1].timestamp).toISOString()}
|
|
`.trim();
|
|
|
|
return {
|
|
content: [{ type: 'text', text: formattedOutput }],
|
|
};
|
|
}
|
|
|
|
return {
|
|
content: [{ type: 'text', text: `Error: ${result.error}` }],
|
|
};
|
|
}
|
|
|
|
// ==========================================
|
|
// Helper Functions
|
|
// ==========================================
|
|
|
|
/**
|
|
* Get appropriate decimal places for price display
|
|
*/
|
|
function getPriceDecimals(symbol: string): number {
|
|
const upper = symbol.toUpperCase();
|
|
|
|
// Stablecoins and fiat pairs
|
|
if (upper.includes('USD') && !upper.startsWith('BTC') && !upper.startsWith('ETH')) {
|
|
return 4;
|
|
}
|
|
|
|
// BTC pairs
|
|
if (upper === 'BTCUSDT' || upper === 'BTCBUSD') {
|
|
return 2;
|
|
}
|
|
|
|
// ETH pairs
|
|
if (upper === 'ETHUSDT' || upper === 'ETHBUSD') {
|
|
return 2;
|
|
}
|
|
|
|
// Small value coins
|
|
if (upper.includes('SHIB') || upper.includes('DOGE') || upper.includes('PEPE')) {
|
|
return 8;
|
|
}
|
|
|
|
// Default
|
|
return 4;
|
|
}
|
|
|
|
/**
|
|
* Format large volume numbers
|
|
*/
|
|
function formatVolume(volume: number): string {
|
|
if (volume >= 1_000_000_000) {
|
|
return `${(volume / 1_000_000_000).toFixed(2)}B`;
|
|
}
|
|
if (volume >= 1_000_000) {
|
|
return `${(volume / 1_000_000).toFixed(2)}M`;
|
|
}
|
|
if (volume >= 1_000) {
|
|
return `${(volume / 1_000).toFixed(2)}K`;
|
|
}
|
|
return volume.toFixed(4);
|
|
}
|