Skip to content

Commit 2689b35

Browse files
Extra commands added
1 parent e60ab98 commit 2689b35

9 files changed

Lines changed: 955 additions & 230 deletions

File tree

package-lock.json

Lines changed: 345 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "",
55
"main": "dist/index.js",
66
"scripts": {
7-
"dev": "ts-node index.ts",
7+
"dev": "nodemon index.ts",
88
"build": "tsc",
99
"start": "node dist/index.js",
1010
"test": "echo \"No tests specified\" && exit 0"
@@ -15,7 +15,9 @@
1515
"dependencies": {
1616
"@google/genai": "^0.9.0",
1717
"axios": "^1.8.4",
18-
"bubble-telegram": "file:",
18+
"chart.js": "^4.4.9",
19+
"chartjs-node-canvas": "^5.0.0",
20+
"chartjs-plugin-datalabels": "^2.2.0",
1921
"dotenv": "^16.5.0",
2022
"express": "^5.1.0",
2123
"mime": "^4.0.7",

services/bot.ts

Lines changed: 346 additions & 226 deletions
Large diffs are not rendered by default.

services/geminiService.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export async function analyzeToken(tokenAddress: string, topHolders: { address?:
2727
Top 5 Holders Information:
2828
${holdersInfo}
2929
30-
Based solely on this token address and holder distribution information, provide a concise 2-3 line verdict on whether this appears to be a good token investment. Consider holder concentration, distribution patterns, and potential red flags. Format your response as a direct recommendation.Use a single emoji at the start to indicate the emotion of the response like danger signals or green signals etc...
30+
Based solely on this token address and holder distribution information, provide a concise 2-3 line verdict on whether this appears to be a good token investment. Consider holder concentration, distribution patterns, and potential red flags. Format your response as a direct recommendation. Use a single emoji at the start to indicate the emotion of the response (e.g., 🟢 for bullish, ⚠️ for caution, 🛑 for bearish, etc)
3131
`;
3232

3333
try {
@@ -42,4 +42,38 @@ export async function analyzeToken(tokenAddress: string, topHolders: { address?:
4242
console.error("Error analyzing token:", error);
4343
return "Analysis failed. Unable to provide recommendation due to technical error.";
4444
}
45-
}
45+
}
46+
47+
export async function analyzeTokenPriceVolume(tokenAddress: string, priceVolumeHistory: { time: number, close: number, volumeUsd: number }[]): Promise<string> {
48+
if (!tokenAddress || !Array.isArray(priceVolumeHistory) || priceVolumeHistory.length === 0) {
49+
return "ERROR: Missing required token address or price/volume history.";
50+
}
51+
52+
const ai = new GoogleGenAI({
53+
apiKey: process.env.GEMINI_API_KEY,
54+
});
55+
56+
// Format data for the prompt
57+
const historyInfo = priceVolumeHistory.map((item, idx) =>
58+
`Period ${idx + 1}: Time: ${new Date(item.time * 1000).toISOString()}, Close: ${item.close}, VolumeUSD: ${item.volumeUsd}`
59+
).join('\n');
60+
61+
const prompt = `
62+
Token Address: ${tokenAddress}
63+
64+
Recent Price and Volume History:
65+
${historyInfo}
66+
67+
Based solely on this price and volume history, provide a concise 2-3 line verdict on the token's recent trend and trading activity. Consider price movement, volume spikes, and any notable patterns. Format your response as a direct recommendation. Use a single emoji at the start to indicate the emotion of the response (e.g., 🚀 for bullish, ⚠️ for caution, 🛑 for bearish, etc).`;
68+
69+
try {
70+
const response = await ai.models.generateContent({
71+
model: "gemini-2.0-flash",
72+
contents: prompt,
73+
});
74+
return response.text || "Analysis failed. Gemini did not return a response.";
75+
} catch (error) {
76+
console.error("Error analyzing token price/volume:", error);
77+
return "Analysis failed. Unable to provide recommendation due to technical error.";
78+
}
79+
}

types/index.ts

Whitespace-only changes.

utils/index.ts

Whitespace-only changes.

utils/lineChart.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { ChartJSNodeCanvas } from 'chartjs-node-canvas';
2+
import { ChartConfiguration } from 'chart.js';
3+
4+
const width = 800;
5+
const height = 400;
6+
7+
const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height });
8+
9+
export async function generateLineChart(
10+
labels: string[],
11+
priceData: number[],
12+
volumeData: number[],
13+
title: string
14+
): Promise<Buffer> {
15+
// Helper to calculate min/max with padding
16+
function getAxisLimits(data: number[]): { min: number, max: number } {
17+
const filtered = data.filter((v) => !isNaN(v));
18+
if (filtered.length === 0) return { min: 0, max: 1 };
19+
const min = Math.min(...filtered);
20+
const max = Math.max(...filtered);
21+
if (min === max) {
22+
// Flat line: add small padding
23+
const pad = min === 0 ? 1 : Math.abs(min) * 0.05;
24+
return { min: min - pad, max: max + pad };
25+
}
26+
const range = max - min;
27+
return { min: min - range * 0.1, max: max + range * 0.1 };
28+
}
29+
const priceLimits = getAxisLimits(priceData);
30+
const volumeLimits = getAxisLimits(volumeData);
31+
const configuration: ChartConfiguration = {
32+
type: 'line',
33+
data: {
34+
labels,
35+
datasets: [
36+
{
37+
label: 'Price',
38+
data: priceData,
39+
borderColor: 'rgb(75, 192, 192)',
40+
tension: 0.0001,
41+
yAxisID: 'y',
42+
},
43+
{
44+
label: 'Volume',
45+
data: volumeData,
46+
borderColor: 'rgb(153, 102, 255)',
47+
tension: 0.0001,
48+
yAxisID: 'y1',
49+
},
50+
],
51+
},
52+
options: {
53+
responsive: true,
54+
interaction: {
55+
mode: 'index',
56+
intersect: false,
57+
},
58+
plugins: {
59+
title: {
60+
display: true,
61+
text: title,
62+
},
63+
legend: {
64+
display: true,
65+
},
66+
tooltip: {
67+
callbacks: {
68+
label: function(context: any) {
69+
// Show up to 8 decimals for small values
70+
return `${context.dataset.label}: ${context.parsed.y.toFixed(8)}`;
71+
}
72+
}
73+
}
74+
},
75+
scales: {
76+
y: {
77+
type: 'linear',
78+
display: true,
79+
position: 'left',
80+
title: {
81+
display: true,
82+
text: 'Price',
83+
},
84+
min: priceLimits.min,
85+
max: priceLimits.max,
86+
},
87+
y1: {
88+
type: 'linear',
89+
display: true,
90+
position: 'right',
91+
title: {
92+
display: true,
93+
text: 'Volume',
94+
},
95+
min: volumeLimits.min,
96+
max: volumeLimits.max,
97+
grid: {
98+
drawOnChartArea: false,
99+
},
100+
},
101+
},
102+
},
103+
};
104+
105+
// If all values are zero or nearly constant, optionally overlay a message
106+
const allZeroOrFlat = (arr: number[]) => arr.every((v) => Math.abs(v - arr[0]) < 1e-12);
107+
if (allZeroOrFlat(priceData) && allZeroOrFlat(volumeData)) {
108+
// Overlay text if the chart would be blank
109+
const buffer = await chartJSNodeCanvas.renderToBuffer(configuration);
110+
// Optionally: add text overlay here if needed (requires more canvas manipulation)
111+
return buffer;
112+
}
113+
return await chartJSNodeCanvas.renderToBuffer(configuration);
114+
}

utils/pieChart.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { ChartJSNodeCanvas } from 'chartjs-node-canvas';
2+
import { ChartConfiguration } from 'chart.js';
3+
import ChartDataLabels from 'chartjs-plugin-datalabels';
4+
import fs from 'fs';
5+
import path from 'path';
6+
7+
// Helper function to generate pie chart
8+
export async function generatePieChart(tokens: any[]) {
9+
const width = 800;
10+
const height = 600;
11+
const chartJSNodeCanvas = new ChartJSNodeCanvas({
12+
width,
13+
height,
14+
chartCallback: (ChartJS) => {
15+
ChartJS.register(ChartDataLabels); // 👈 Important for showing labels
16+
}
17+
});
18+
19+
const labels = tokens.map(token => token.symbol);
20+
const data = tokens.map(token => Number(token.valueUsd));
21+
22+
const total = data.reduce((sum, value) => sum + value, 0); // Total value for percentage calculation
23+
24+
const backgroundColors = [
25+
'#4e79a7', '#f28e2c', '#e15759', '#76b7b2',
26+
'#59a14f', '#edc949', '#af7aa1', '#ff9da7',
27+
'#9c755f', '#bab0ab'
28+
];
29+
30+
const configuration: ChartConfiguration<'pie', number[], unknown> = {
31+
type: 'pie',
32+
data: {
33+
labels,
34+
datasets: [{
35+
data,
36+
backgroundColor: backgroundColors,
37+
borderWidth: 1,
38+
borderColor: '#ffffff',
39+
}]
40+
},
41+
options: {
42+
responsive: true,
43+
plugins: {
44+
legend: {
45+
position: 'bottom',
46+
},
47+
title: {
48+
display: true,
49+
text: 'Token Balance Distribution',
50+
font: {
51+
size: 24,
52+
}
53+
},
54+
datalabels: {
55+
color: '#000000', // black for better visibility
56+
align: 'center', // center the label inside the pie slice
57+
anchor: 'center', // anchor the label in the center of the slice
58+
formatter: (value: number, context: any) => {
59+
const label = context.chart.data.labels?.[context.dataIndex] || '';
60+
const percentage = (value / total) * 100;
61+
62+
// Show label only if percentage is >= 10%
63+
if (percentage >= 10) {
64+
return `${label}: $${value.toFixed(2)}\n(${percentage.toFixed(2)}%)`;
65+
}
66+
return ''; // Hide label for smaller segments
67+
},
68+
font: {
69+
weight: 'bold',
70+
size: 12,
71+
},
72+
clamp: true,
73+
textAlign:'center'
74+
},
75+
},
76+
layout: {
77+
padding: 20,
78+
}
79+
}
80+
};
81+
82+
const image = await chartJSNodeCanvas.renderToBuffer(configuration);
83+
84+
const filePath = path.join(__dirname, `token_balance_chart_${Date.now()}.png`);
85+
fs.writeFileSync(filePath, image);
86+
return filePath;
87+
}

utils/sendLargeMessage.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const TELEGRAM_MESSAGE_LIMIT = 4096; // Telegram's official limit
2+
3+
export async function sendLargeMessage(bot: any, chatId: number, message: string, options: any) {
4+
const chunks: string[] = [];
5+
let currentChunk = '';
6+
7+
const tokens = message.split('\n\n'); // Split at end of each token block
8+
9+
for (const token of tokens) {
10+
if ((currentChunk + token + '\n\n').length > TELEGRAM_MESSAGE_LIMIT) {
11+
chunks.push(currentChunk);
12+
currentChunk = '';
13+
}
14+
currentChunk += token + '\n\n';
15+
}
16+
17+
if (currentChunk) chunks.push(currentChunk);
18+
19+
for (let i = 0; i < chunks.length; i++) {
20+
const chunkMessage = i === 0 ? chunks[i] : `...continued\n\n${chunks[i]}`;
21+
await bot.sendMessage(chatId, chunkMessage.trim(), options);
22+
}
23+
}

0 commit comments

Comments
 (0)