// index.js (Node.js Telegram Bot & Webhook Handler - CORRECTED) const express = require('express'); const TelegramBot = require('node-telegram-bot-api'); const fs = require('fs').promises; const path = require('path'); process.env.NTBA_FIX_350 = 'true'; // --- Configuration --- const BOT_TOKEN = '8026490326:AAHUvglVeT3-3zyiu2a7Xga4glwtTHDJjXs'; // Your Bot Token const CHAT_ID = '7526666473'; // Your Chat ID const WEBHOOK_PORT = 81; // Port for optional TradingView webhooks const INTERNAL_SIGNAL_PORT = 82; // Port for internal signals from Python scanner // Corrected directory for all operational files const STREAMLINE_DIR = 'C:\\trading-bot\\streamline'; const ORDER_FILE_PATH = path.join(STREAMLINE_DIR, 'order_fast.json'); const ACCOUNT_DETAILS_PATH = path.join('C:', 'trading-bot', 'account_details.txt'); // Input file, location is fine const SYMBOLS_CONFIG_PATH = path.join('C:', 'trading-bot', 'symbols_config.json'); // Input file, location is fine const ACTIVE_LISTENERS_FILE = path.join(STREAMLINE_DIR, 'active_listeners.json'); // --- Global State --- let activeListeners = {}; // { "SYMBOL_TF_DIRECTION": { symbol, direction, tf, startTime }, ... } let symbolCategories = {}; let allSymbols = []; let m1Accounts = []; const userState = {}; // Keyed by CHAT_ID // --- Initialize Express Apps and Bot --- const app = express(); // For optional TradingView webhooks app.use(express.json()); const internalApp = express(); // For Python scanner signals internalApp.use(express.json()); const bot = new TelegramBot(BOT_TOKEN, { polling: true }); // Polling starts immediately // --- Utility Functions --- async function loadSymbolsAndCategories() { try { const data = await fs.readFile(SYMBOLS_CONFIG_PATH, 'utf8'); const config = JSON.parse(data); const categories = {}; const symbols = new Set(); if (config.symbols && Array.isArray(config.symbols)) { config.symbols.forEach(s => { if (s.name && typeof s.name === 'string') { symbols.add(s.name); const categoryKey = s.telegram_category || 'general'; if (!categories[categoryKey]) { const catConfig = config.telegram_categories_config && config.telegram_categories_config[categoryKey] ? config.telegram_categories_config[categoryKey] : { name: categoryKey.charAt(0).toUpperCase() + categoryKey.slice(1), emoji: '❔' }; categories[categoryKey] = { name: catConfig.name, emoji: catConfig.emoji, symbols: [] }; } categories[categoryKey].symbols.push(s.name); } }); } symbolCategories = categories; allSymbols = Array.from(symbols); console.log('Successfully loaded symbols and categories from symbols_config.json'); } catch (error) { console.error('Error loading symbols_config.json:', error.message); symbolCategories = { general: { name: "Symbols", emoji: "❔", symbols: ["EURUSD", "BTCUSD"] } }; // Fallback allSymbols = ["EURUSD", "BTCUSD"]; } } async function loadAccounts() { let accounts = []; try { const data = await fs.readFile(ACCOUNT_DETAILS_PATH, 'utf8'); const lines = data.replace(/\r/g, "").split('\n').filter(line => line.trim() && !line.startsWith('#')); for (const line of lines) { const parts = line.split(',').map(part => part.trim()); const accountDict = {}; for (const part of parts) { if (part.includes(':')) { const [key, value] = part.split(':').map(s => s.trim()); accountDict[key] = value; } } if (accountDict.type === 'M1' && accountDict.login) { accounts.push(accountDict.login); } } console.log('Loaded M1 accounts:', accounts); return accounts; } catch (error) { console.error('Error loading account_details.txt:', error.message); return []; } } async function saveActiveListeners() { try { await fs.writeFile(ACTIVE_LISTENERS_FILE, JSON.stringify(activeListeners, null, 2)); console.log('Active listeners saved to file.'); } catch (error) { console.error('Error saving active listeners:', error); } } async function loadActiveListeners() { try { if (await fs.stat(ACTIVE_LISTENERS_FILE).catch(() => false)) { const data = await fs.readFile(ACTIVE_LISTENERS_FILE, 'utf8'); const loaded = JSON.parse(data); if (typeof loaded === 'object' && loaded !== null) { activeListeners = loaded; console.log('Active listeners loaded from file.'); } else { console.warn('Active listeners file was not a valid object, starting fresh.'); activeListeners = {}; } } else { console.log('No active listeners file found, starting fresh.'); activeListeners = {}; } } catch (error) { console.error('Error loading active listeners:', error.message); activeListeners = {}; } } // --- Inline Keyboards --- const getMainMenuKeyboard = () => { const listeningSymbolsCount = Object.keys(activeListeners).length; const stopListeningButtonText = listeningSymbolsCount > 0 ? `🚫 Stop Listening (${listeningSymbolsCount})` : '🚫 Stop Listening'; return { reply_markup: { inline_keyboard: [ [{ text: 'πŸ›’ Buy (ATR10)', callback_data: 'action_buy' }, { text: 'πŸ“‰ Sell (ATR10)', callback_data: 'action_sell' }], [{ text: 'πŸ‘‚ Listen Long (Scanner)', callback_data: 'action_listen_long' }, { text: 'πŸ‘‚ Listen Short (Scanner)', callback_data: 'action_listen_short' }], [{ text: stopListeningButtonText, callback_data: 'action_stop_listening' }], [{ text: '❌ Close Trades for Symbol', callback_data: 'action_close_select_symbol' }], [{ text: 'ℹ️ Account Info', callback_data: 'action_account_info' }], [{ text: 'πŸ”„ Start Over', callback_data: 'action_start_over' }] ] } }; }; const getTimeframeSelectionKeyboard = (actionContext, symbol) => { const timeframes = ["1", "5", "15", "30", "60", "240", "D", "W", "M"]; const keyboardRows = []; const MAX_BUTTONS_PER_ROW = 3; let currentButtons = []; timeframes.forEach(tf => { currentButtons.push({ text: tf, callback_data: `tf_${tf}` }); if (currentButtons.length === MAX_BUTTONS_PER_ROW) { keyboardRows.push([...currentButtons]); currentButtons = []; } }); if (currentButtons.length > 0) keyboardRows.push([...currentButtons]); keyboardRows.push([{ text: '⬅️ Back to Symbol List', callback_data: `back_to_symbols_${actionContext.split('_').pop()}` }]); // e.g. back_to_symbols_long keyboardRows.push([{ text: 'πŸ”„ Start Over', callback_data: 'action_start_over' }]); return { text: `Select *TIMEFRAME* for *${symbol}* to *${actionContext.toUpperCase().replace("LISTEN_", "")}* signals:`, keyboard: { reply_markup: { inline_keyboard: keyboardRows } } }; }; const getSymbolSelectionKeyboardDetails = (action) => { const keyboardRows = []; let messageContent = `Please select a *SYMBOL*`; const MAX_BUTTONS_PER_ROW = 3; let symbolsToDisplay = []; if (action === 'stop_listening') { messageContent += ` to *STOP LISTENING* for:`; symbolsToDisplay = Object.values(activeListeners).map(l => ({ text: `${l.symbol} ${l.tf}' (${l.direction})`, value: `${l.symbol}_${l.tf}_${l.direction}` })); if (symbolsToDisplay.length === 0) { return { text: "No symbols are currently being listened to.", keyboard: { reply_markup: { inline_keyboard: [[{ text: '⬅️ Back to Main Menu', callback_data: 'back_to_main_menu' }]] } } }; } } else { const actionVerb = action.replace('listen_', '').toUpperCase(); messageContent += ` to *${actionVerb}* signals:\n`; if (allSymbols.length === 0) { // Should be caught by loadSymbolsAndCategories error handling return { text: "Error: Symbol list not available.", keyboard: {reply_markup: { inline_keyboard: [[{ text: '⬅️ Back to Main Menu', callback_data: 'back_to_main_menu' }]]}}}; } symbolsToDisplay = allSymbols.map(s => { let emoji = '❔'; for (const catKey in symbolCategories) { if (symbolCategories[catKey] && symbolCategories[catKey].symbols.includes(s)) { emoji = symbolCategories[catKey].emoji; break; } } return { text: `${emoji} ${s}`, value: s }; }); } let currentButtons = []; symbolsToDisplay.forEach(item => { currentButtons.push({ text: item.text, callback_data: `symbol_${item.value}` }); if (currentButtons.length === MAX_BUTTONS_PER_ROW) { keyboardRows.push([...currentButtons]); currentButtons = []; } }); if (currentButtons.length > 0) keyboardRows.push([...currentButtons]); messageContent += `\n--------------------\nOr go back:`; keyboardRows.push([{ text: '⬅️ Back to Main Menu', callback_data: 'back_to_main_menu' }]); return { text: messageContent.trim(), keyboard: { reply_markup: { inline_keyboard: keyboardRows } } }; }; const getCloseSymbolConfirmKeyboard = (symbol) => ({ reply_markup: { inline_keyboard: [ [{ text: `βœ… Yes, Close All for ${symbol}`, callback_data: 'confirm_close_symbol_yes' }], [{ text: '❌ No (Back to Symbol List)', callback_data: 'cancel_close_back_to_symbols' }], [{ text: 'πŸ”„ Start Over (Main Menu)', callback_data: 'action_start_over' }] ] } }); // --- Message Sending and Bot Interaction Logic --- async function sendOrEditMessage(chatId, messageId, text, keyboard) { try { if (messageId) { return await bot.editMessageText(text, { chat_id: chatId, message_id: messageId, ...keyboard, parse_mode: 'Markdown' }); } else { return await bot.sendMessage(chatId, text, { ...keyboard, parse_mode: 'Markdown' }); } } catch (error) { console.error(`Error sending/editing message: ${error.message}. Text: "${text}"`); if (messageId && (error.message.includes('message is not modified') || error.message.includes('message to edit not found'))) { return { message_id: messageId }; } try { // Fallback to sending new message return await bot.sendMessage(chatId, text, { ...keyboard, parse_mode: 'Markdown' }); } catch (fallbackError) { console.error(`Fallback send message also failed: ${fallbackError.message}`); return null; } } } async function sendErrorNotification(errorMessageText) { try { await bot.sendMessage(CHAT_ID, `🚨 *Trading Bot Error* 🚨\n\n${errorMessageText}`, { parse_mode: 'Markdown' }); } catch (error) { console.error(`Error sending error notification: ${error.message}`); } } async function resetToMainMenu(chatId, existingMessageId = null) { userState[chatId] = { step: 'main', messageId: existingMessageId, action: null, symbol: null, tf: null }; const text = 'Please select an action:'; const keyboard = getMainMenuKeyboard(); const sentMessage = await sendOrEditMessage(chatId, existingMessageId, text, keyboard); if (sentMessage && sentMessage.message_id) userState[chatId].messageId = sentMessage.message_id; } // --- Trade Command Processing --- async function processTradeCommand(selectedSymbol, orderData, operationType, currentMessageId) { if (!m1Accounts || m1Accounts.length === 0) { const errorMsg = `Cannot ${operationType}: No M1 accounts loaded.`; console.error(errorMsg); await bot.sendMessage(CHAT_ID, `Error: ${errorMsg}`); if (currentMessageId) await resetToMainMenu(CHAT_ID, currentMessageId); return; } const isWebhookTrigger = !currentMessageId; const orderTimestamp = orderData.timestamp; try { await fs.writeFile(ORDER_FILE_PATH, JSON.stringify(orderData)); // ORDER_FILE_PATH is already correct console.log(`${operationType} order for ${selectedSymbol} saved to ${ORDER_FILE_PATH}:`, orderData); await bot.sendMessage(CHAT_ID, `${operationType === 'entry' ? 'Order' : 'Close command'} sent to MT5 for *${selectedSymbol}*...`, { parse_mode: 'Markdown' }); let allDone = false; const timeoutSeconds = 20; for (let i = 0; i < timeoutSeconds; i++) { const doneFilePromises = m1Accounts.map(acc => fs.stat(path.join(STREAMLINE_DIR, `done_fast_${acc}_${orderTimestamp}.txt`)).then(() => true).catch(() => false) // Corrected path ); const doneFileResults = await Promise.all(doneFilePromises); if (doneFileResults.every(done => done)) { allDone = true; break; } await new Promise(resolve => setTimeout(resolve, 1000)); } if (allDone) console.log(`All accounts reported done for ${operationType} on ${selectedSymbol} (Order ${orderTimestamp}).`); else console.warn(`Timeout or not all accounts reported done for ${operationType} on ${selectedSymbol} (Order ${orderTimestamp}).`); let combinedFeedbackContent = `--- Combined Feedback for Order ${orderTimestamp} ---\nSymbol: ${selectedSymbol}\nOperation: ${operationType.toUpperCase()}\n\n`; let feedbackFilesFound = 0; const individualFeedbackFilePathsToDelete = []; for (const acc of m1Accounts) { const individualFeedbackPath = path.join(STREAMLINE_DIR, `trade_feedback_fast_${acc}_${orderTimestamp}.txt`); // Corrected path try { if (await fs.stat(individualFeedbackPath).catch(() => false)) { const content = await fs.readFile(individualFeedbackPath, 'utf8'); combinedFeedbackContent += `--- Account: ${acc} ---\n${content}\n\n`; feedbackFilesFound++; individualFeedbackFilePathsToDelete.push(individualFeedbackPath); } } catch (readError) { console.error(`Error reading individual feedback file ${individualFeedbackPath}: ${readError.message}`); } } if (feedbackFilesFound > 0) { const tempCombinedFeedbackFilename = `COMBINED_feedback_${orderTimestamp}.txt`; const tempCombinedFeedbackFilePath = path.join(STREAMLINE_DIR, tempCombinedFeedbackFilename); // Corrected path await fs.writeFile(tempCombinedFeedbackFilePath, combinedFeedbackContent.trim()); await bot.sendDocument(CHAT_ID, tempCombinedFeedbackFilePath, { caption: `${operationType} Feedback for ${selectedSymbol}`, contentType: 'text/plain' }); await fs.unlink(tempCombinedFeedbackFilePath).catch(err => console.error(`Error unlinking temp combined feedback: ${err.message}`)); for (const p of individualFeedbackFilePathsToDelete) { await fs.unlink(p).catch(err => console.error(`Error unlinking individual feedback: ${err.message}`)); } } else { const msg = allDone ? `No feedback files found for order ${orderTimestamp}.` : `No feedback received for ${selectedSymbol} (Order ${orderTimestamp}).`; await bot.sendMessage(CHAT_ID, msg); if (!allDone) await sendErrorNotification(`Not all accounts completed ${operationType} for ${selectedSymbol} (Order ${orderTimestamp}).`); } await fs.unlink(ORDER_FILE_PATH).catch(err => console.warn(`Could not delete ${ORDER_FILE_PATH}: ${err.message}`)); for (const acc of m1Accounts) { await fs.unlink(path.join(STREAMLINE_DIR, `done_fast_${acc}_${orderTimestamp}.txt`)).catch(() => {}); // Corrected path } } catch (error) { console.error(`Error during ${operationType} for ${selectedSymbol} (Order ${orderTimestamp}):`, error); await bot.sendMessage(CHAT_ID, `Error during ${operationType} for *${selectedSymbol}*: ${error.message}`, { parse_mode: 'Markdown' }); await sendErrorNotification(`${operationType} failed for ${selectedSymbol} (Order ${orderTimestamp}): ${error.message}`); } finally { if (!isWebhookTrigger && currentMessageId) await resetToMainMenu(CHAT_ID, currentMessageId); } } async function executeTradeOrder(action, symbol, messageId) { const timestamp = Math.floor(Date.now() / 1000); const orderData = { symbol, action: 'entry', direction: action.toUpperCase(), sl_type: 'atr10', timestamp }; await processTradeCommand(symbol, orderData, 'entry', messageId); } async function executeClosePositions(symbol, messageId) { const timestamp = Math.floor(Date.now() / 1000); const orderData = { symbol, action: 'close', timestamp }; await processTradeCommand(symbol, orderData, 'close', messageId); } // --- Automated Modification Feedback --- const modificationFeedbackInterval = 15000; // 15 seconds async function checkAndSendModificationFeedback() { // This function's original file interactions were not detailed. // Based on the new workflow, direct modification feedback to Telegram is minimized. // If it needs to check files created by Python executors for mods, paths should use STREAMLINE_DIR. // For now, this is a placeholder. // console.log(`Periodic check run at ${new Date().toISOString()} (checkAndSendModificationFeedback)`); } // --- Account Info --- async function handleAccountInfoCommand(msg, existingMessageId = null) { const chatId = msg.chat.id.toString(); if (chatId !== CHAT_ID) return; const messageIdToUse = existingMessageId || (msg.message ? msg.message.message_id : null); if (!m1Accounts || m1Accounts.length === 0) { await bot.sendMessage(chatId, "No M1 accounts loaded to fetch info for."); if (messageIdToUse) await resetToMainMenu(chatId, messageIdToUse); return; } await bot.sendMessage(chatId, "Requesting account information from all instances... Please wait.", { parse_mode: 'Markdown' }); const requestFileName = 'account_info_request.txt'; const requestFilePath = path.join(STREAMLINE_DIR, requestFileName); // Corrected path try { // Create the request file await fs.writeFile(requestFilePath, `Request from Telegram Bot at ${new Date().toISOString()}`); console.log(`Account info request file created: ${requestFilePath}`); // Wait for responses await new Promise(resolve => setTimeout(resolve, 15000)); // Wait 15 seconds for responses let combinedResponse = "--- Account Information ---\n\n"; let filesProcessed = 0; let totalPL = 0; let firstCurrency = null; const responseFilePattern = /^account_info_resp_(\d+)_(\w+)\.txt$/; const filesInStreamline = await fs.readdir(STREAMLINE_DIR); // Corrected path for (const file of filesInStreamline) { if (responseFilePattern.test(file)) { const filePath = path.join(STREAMLINE_DIR, file); // Corrected path try { const content = await fs.readFile(filePath, 'utf8'); combinedResponse += content + "\n--------------------\n"; //η°‘ζ˜“ηš„γ«P/Lγ‚’γƒ‘γƒΌγ‚ΉοΌˆPythonε΄γ§ζ•΄ε½’γ•γ‚Œγ¦γ„γ‚‹ε‰ζοΌ‰ const plMatch = content.match(/PL:\s*([-\d.]+)\s*(\w+)/); if (plMatch && plMatch[1] && plMatch[2]) { totalPL += parseFloat(plMatch[1]); if (!firstCurrency) firstCurrency = plMatch[2]; } await fs.unlink(filePath); // Delete after processing filesProcessed++; } catch (readErr) { console.error(`Error reading or unlinking response file ${filePath}:`, readErr.message); } } } if (filesProcessed > 0) { combinedResponse += `\n\n--- AGGREGATED P/L ---`; combinedResponse += `\nTotal Floating P/L from ${filesProcessed} account(s): ${totalPL.toFixed(2)} ${firstCurrency || 'N/A'}`; const tempCombinedFilename = `ACCOUNTS_SUMMARY_${Date.now()}.txt`; const tempCombinedPath = path.join(STREAMLINE_DIR, tempCombinedFilename); // Corrected path await fs.writeFile(tempCombinedPath, combinedResponse); await bot.sendDocument(chatId, tempCombinedPath, { caption: "Combined Account Summary" }); await fs.unlink(tempCombinedPath).catch(e => console.warn(`Could not delete temp summary: ${e.message}`)); } else { await bot.sendMessage(chatId, "No account information responses received within the timeframe."); } // Clean up the request file await fs.unlink(requestFilePath).catch(e => console.warn(`Could not delete request file: ${e.message}`)); } catch (error) { console.error("Error in handleAccountInfoCommand:", error); await bot.sendMessage(chatId, "An error occurred while fetching account information."); if (requestFilePath && await fs.stat(requestFilePath).catch(()=>false)) { await fs.unlink(requestFilePath).catch(e => console.warn(`Could not delete request file on error: ${e.message}`)); } } finally { if (messageIdToUse) await resetToMainMenu(chatId, messageIdToUse); } } // --- Main Application Logic --- async function main() { try { console.log("Starting bot initialization..."); await loadSymbolsAndCategories(); await loadActiveListeners(); m1Accounts = await loadAccounts(); // Assign to global m1Accounts if (!m1Accounts || m1Accounts.length === 0) { console.error("CRITICAL: No M1 accounts loaded. Bot may not function correctly for trades."); } console.log("Initial data loaded."); // Setup Telegram bot listeners AFTER essential data is loaded bot.onText(/\/start/i, async (msg) => { const chatId = msg.chat.id.toString(); if (chatId !== CHAT_ID) return; console.log("/start command received from chat", chatId); await resetToMainMenu(chatId, msg.message_id); // Pass message_id to potentially edit }); bot.onText(/\/acc/i, (msg) => handleAccountInfoCommand(msg, msg.message_id)); bot.on('callback_query', async (query) => { const chatId = query.message.chat.id.toString(); if (chatId !== CHAT_ID) return; const command = query.data; const messageId = query.message.message_id; try { await bot.answerCallbackQuery(query.id); } catch (ansError) { console.error(`Error answering cb query ${query.id}: ${ansError.message}`); } let state = userState[chatId] || { step: 'main', messageId: messageId, action: null, symbol: null, tf: null }; state.messageId = messageId; // Always update messageId let text, keyboard; if (command === 'action_start_over' || command === 'back_to_main_menu') { await resetToMainMenu(chatId, messageId); return; } if (command.startsWith('back_to_symbols_')) { state.action = command.substring('back_to_symbols_'.length); // e.g., 'long', 'short' state.step = 'select_symbol'; const details = getSymbolSelectionKeyboardDetails(state.action); text = details.text; keyboard = details.keyboard; } else if (state.step === 'main') { if (['action_buy', 'action_sell', 'action_close_select_symbol', 'action_listen_long', 'action_listen_short', 'action_stop_listening'].includes(command)) { if (command === 'action_close_select_symbol') state.action = 'close'; else if (command === 'action_listen_long') state.action = 'listen_long'; else if (command === 'action_listen_short') state.action = 'listen_short'; else if (command === 'action_stop_listening') state.action = 'stop_listening'; else state.action = command.split('_')[1]; // 'buy' or 'sell' state.step = 'select_symbol'; const details = getSymbolSelectionKeyboardDetails(state.action); text = details.text; keyboard = details.keyboard; } else if (command === 'action_account_info') { await handleAccountInfoCommand(query.message, messageId); return; } } else if (state.step === 'select_symbol') { if (command.startsWith('symbol_')) { const dataPart = command.substring('symbol_'.length); state.symbol = dataPart.split('_')[0]; // Extract symbol if it's SYMBOL_TF_DIR format if (state.action === 'buy' || state.action === 'sell') { await executeTradeOrder(state.action, state.symbol, state.messageId); return; } else if (state.action === 'close') { state.step = 'confirm_close_for_symbol'; text = `*Confirm Close*\nAction: CLOSE ALL TRADES\nSymbol: *${state.symbol}*`; keyboard = getCloseSymbolConfirmKeyboard(state.symbol); } else if (state.action === 'listen_long' || state.action === 'listen_short') { // state.symbol is already set from dataPart state.step = 'select_timeframe'; const details = getTimeframeSelectionKeyboard(state.action, state.symbol); text = details.text; keyboard = details.keyboard; } else if (state.action === 'stop_listening') { const [symbolToStop, tfToStop, directionToStop] = dataPart.split('_'); const listenerKey = `${symbolToStop.toUpperCase()}_${tfToStop}_${directionToStop.toUpperCase()}`; if (activeListeners[listenerKey]) { delete activeListeners[listenerKey]; await saveActiveListeners(); text = `🚫 Stopped listening for *${directionToStop}* on *${symbolToStop} (${tfToStop}')*.`; } else { text = `⚠️ Was not listening for *${directionToStop}* on *${symbolToStop} (${tfToStop}')*.`; } keyboard = getMainMenuKeyboard(); state.step = 'main'; } } } else if (state.step === 'select_timeframe') { if (command.startsWith('tf_')) { state.tf = command.substring('tf_'.length); const listenDirection = state.action === 'listen_long' ? 'LONG' : 'SHORT'; const listenerKey = `${state.symbol.toUpperCase()}_${state.tf}_${listenDirection}`; if (activeListeners[listenerKey]) { text = `⚠️ Already listening for *${listenDirection}* on *${state.symbol} (${state.tf}')*.`; } else { activeListeners[listenerKey] = { symbol: state.symbol.toUpperCase(), direction: listenDirection, tf: state.tf, startTime: Date.now() }; await saveActiveListeners(); text = `πŸ‘‚ Now listening for *${listenDirection}* on *${state.symbol} (${state.tf}')*. Scanner will check.`; } keyboard = getMainMenuKeyboard(); state.step = 'main'; } } else if (state.step === 'confirm_close_for_symbol') { if (command === 'confirm_close_symbol_yes') { await executeClosePositions(state.symbol, state.messageId); return; } else if (command === 'cancel_close_back_to_symbols') { state.step = 'select_symbol'; state.action = 'close'; // Ensure action is still close const details = getSymbolSelectionKeyboardDetails('close'); text = details.text; keyboard = details.keyboard; state.symbol = null; } } userState[chatId] = state; // Save updated state if (text && keyboard) { const sentMessage = await sendOrEditMessage(chatId, messageId, text, keyboard); if (sentMessage && sentMessage.message_id) userState[chatId].messageId = sentMessage.message_id; } else if (!text && !keyboard && state.step !== 'main' && !(state.action === 'buy' || state.action === 'sell' || state.action === 'close' && command === 'confirm_close_symbol_yes')) { console.warn(`Unhandled state/command: ${state.step}/${command}. Resetting.`); await resetToMainMenu(chatId, messageId); } }); // --- Define and Start Web Servers --- async function handleInternalSignal(req, res) { const signalData = req.body; console.log('Internal signal received:', signalData); if (!signalData || !signalData.symbol || !signalData.direction || !signalData.tf) { return res.status(400).send('Invalid signal data: symbol, direction, and tf required.'); } const { symbol: sigSymbol, direction: sigDirection, tf: sigTf } = signalData; const listenerKey = `${sigSymbol.toUpperCase()}_${sigTf}_${sigDirection.toUpperCase()}`; if (activeListeners[listenerKey]) { const listenerDetails = activeListeners[listenerKey]; const timestamp = Math.floor(Date.now() / 1000); const orderData = { symbol: listenerDetails.symbol, action: 'entry', direction: listenerDetails.direction === 'LONG' ? 'BUY' : 'SELL', sl_type: 'atr10', timestamp }; try { await bot.sendMessage(CHAT_ID, `πŸ”” *SCANNER SIGNAL!*\n*${sigDirection}* for *${sigSymbol} (${sigTf}')*. Executing...`, { parse_mode: 'Markdown' }); await processTradeCommand(listenerDetails.symbol, orderData, 'entry', null); delete activeListeners[listenerKey]; await saveActiveListeners(); await bot.sendMessage(CHAT_ID, `Listener for *${sigSymbol} ${sigTf}' ${sigDirection}* executed & inactive.`, { parse_mode: 'Markdown' }); res.status(200).send('Internal signal processed.'); } catch (error) { console.error(`Internal Signal Error for ${listenerKey}:`, error); await bot.sendMessage(CHAT_ID, `⚠️ Error on internal signal *${sigSymbol} ${sigTf}' ${sigDirection}*: ${error.message}`, { parse_mode: 'Markdown' }); res.status(500).send('Error processing internal signal.'); } } else { res.status(200).send('Internal signal received, no active listener.'); } } app.post('/webhook_fast', async (req, res) => { console.log('TradingView Webhook (optional) received:', JSON.stringify(req.body)); // Implement TradingView webhook logic here if needed, ensuring any files created // (like order_fast.json) use ORDER_FILE_PATH (which points to STREAMLINE_DIR). // For example, if req.body contains order details: // const { symbol, action, direction } = req.body; // Simplified example // if (symbol && action && direction) { // const timestamp = Math.floor(Date.now() / 1000); // const orderData = { symbol, action, direction, sl_type: 'atr10', timestamp, source: 'webhook' }; // await processTradeCommand(symbol, orderData, action === 'entry' ? 'entry' : 'close', null); // } res.status(200).send('TV Webhook received.'); }); app.listen(WEBHOOK_PORT, () => console.log(`Optional TradingView Webhook server on port ${WEBHOOK_PORT}`)); internalApp.post('/internal_signal', handleInternalSignal); internalApp.listen(INTERNAL_SIGNAL_PORT, () => console.log(`Internal Signal server (Python) on port ${INTERNAL_SIGNAL_PORT}`)); // Periodic check for modification feedback (if any file-based checks are re-enabled) setInterval(() => { if (m1Accounts && m1Accounts.length > 0) { checkAndSendModificationFeedback().catch(err => console.error("Err in mod feedback check:", err.message)); } }, modificationFeedbackInterval); console.log("Bot is fully initialized and ready for commands."); bot.sendMessage(CHAT_ID, "πŸ€– Trading Bot (Scanner Ready) is online! Send /start to begin.") .catch(error => console.error("Failed to send startup message:", error.message)); } catch (error) { console.error("CRITICAL ERROR DURING BOT INITIALIZATION:", error); process.exit(1); // Exit if essential setup fails } } main(); // Start the application