Bot & Automation
Market AI
/root/hermes-projects/Market AI
src/bot/handlers/registerHandlers.ts
text
import type { Context, Telegraf } from "telegraf";
import { detectMarketSymbolFromText, findMarketSymbol } from "../../constants/markets";
import type {
BrokerExecutionStatus,
MarketSymbol
} from "../../domain/types";
import { env } from "../../config/env";
import type { TradingService } from "../../services/tradingService";
import type { AutoTradingService } from "../../services/autoTradingService";
import { exponentialMovingAverage } from "../../utils/math";
import {
mainMenuKeyboard,
marketSelectionKeyboard,
marketAnalysisKeyboard,
autoTradingDashboardKeyboard,
setupAccountKeyboard,
setupMarketKeyboard,
setupStrategyKeyboard,
setupRiskKeyboard,
setupReviewKeyboard,
setupRealConfirmKeyboard
} from "../ui/keyboards";
import {
formatWelcome,
formatMarketAnalysis,
formatAutoTradingDashboard,
formatSetupRisk,
formatSetupReview,
formatRealConfirmation,
formatHelp,
formatWatchlist,
formatSettings
} from "../ui/formatters";
// State session in-memory untuk menyimpan data konfigurasi setup Auto Trading per Chat ID
interface AutoTradingSetupSession {
step: "account" | "market" | "strategy" | "risk" | "review";
accountMode: "demo" | "real";
symbolCode: string;
strategy: "fast_scalping" | "trend_scalping" | "custom";
lot: number;
maxPosition: number;
targetProfitPips: number;
stopLossPips: number;
dailyLossLimitPercent: number;
cooldownSeconds: number;
maxSpreadPips: number;
waitingInput?: "lot" | "tp" | "sl" | "dailyloss" | "cooldown";
}
const setupSessions = new Map<number, AutoTradingSetupSession>();
export function registerHandlers(
bot: Telegraf,
tradingService: TradingService,
autoTradingService?: AutoTradingService
): void {
// ─── Command Handlers ──────────────────────────────────────────────────────
bot.start(async (ctx) => {
if (!isAllowedUser(ctx.from?.id)) {
await ctx.reply("Akses bot tidak diizinkan untuk user ini.");
return;
}
await ctx.reply(formatWelcome(env.botDisplayName), mainMenuKeyboard());
});
bot.help(async (ctx) => {
await ctx.reply(formatHelp(), mainMenuKeyboard());
});
bot.command("settings", async (ctx) => {
await ctx.reply(await buildSettingsMessage(tradingService), mainMenuKeyboard());
});
bot.command("auto", async (ctx) => {
const isEnabled = autoTradingService?.isEnabled() ?? false;
await ctx.reply(
buildAutoTradingMessage(autoTradingService),
autoTradingDashboardKeyboard(isEnabled)
);
});
bot.command("watchlist", async (ctx) => {
const chatId = ctx.chat.id;
const symbols = await tradingService.getWatchlist(chatId);
await ctx.reply(formatWatchlist(symbols), mainMenuKeyboard());
});
// ─── Inline Keyboard Menu Navigation Handlers ────────────────────────────────
bot.action("menu:main", async (ctx) => {
await ctx.answerCbQuery();
await ctx.reply(formatWelcome(env.botDisplayName), mainMenuKeyboard());
});
bot.action("menu:market", async (ctx) => {
await ctx.answerCbQuery();
await ctx.reply("📊 *Pilih Market yang Ingin Dipantau:*", marketSelectionKeyboard());
});
bot.action("menu:watchlist", async (ctx) => {
await ctx.answerCbQuery();
const symbols = await tradingService.getWatchlist(ctx.chat?.id ?? 0);
await ctx.reply(formatWatchlist(symbols), mainMenuKeyboard());
});
bot.action("menu:alert", async (ctx) => {
await ctx.answerCbQuery();
await ctx.reply(
"🔔 *Price Alert*:\nFitur scheduler realtime terintegrasi sedang berjalan di background. Anda dapat mengatur alert melalui detail Market.",
mainMenuKeyboard()
);
});
bot.action("menu:settings", async (ctx) => {
await ctx.answerCbQuery();
await ctx.reply(await buildSettingsMessage(tradingService), mainMenuKeyboard());
});
bot.action("menu:help", async (ctx) => {
await ctx.answerCbQuery();
await ctx.reply(formatHelp(), mainMenuKeyboard());
});
// ─── Market Analysis & Action Handlers ─────────────────────────────────────
bot.action("menu:auto", async (ctx) => {
await ctx.answerCbQuery();
const isEnabled = autoTradingService?.isEnabled() ?? false;
await ctx.reply(
buildAutoTradingMessage(autoTradingService),
autoTradingDashboardKeyboard(isEnabled)
);
});
bot.action(/^pair:(.+)$/, async (ctx) => {
await ctx.answerCbQuery();
const symbolCode = ctx.match[1];
await sendMarketAnalysisScreen(ctx, tradingService, symbolCode);
});
bot.action(/^market:refresh:(.+)$/, async (ctx) => {
await ctx.answerCbQuery();
const symbolCode = ctx.match[1];
await sendMarketAnalysisScreen(ctx, tradingService, symbolCode);
});
bot.action(/^market:autotrade:(.+)$/, async (ctx) => {
await ctx.answerCbQuery();
const symbolCode = ctx.match[1];
const chatId = ctx.chat?.id ?? 0;
// Lompat langsung ke Step 1 Setup dengan market terpilih
const session: AutoTradingSetupSession = {
step: "account",
accountMode: "demo",
symbolCode,
strategy: "fast_scalping",
lot: 0.01,
maxPosition: 1,
targetProfitPips: 15,
stopLossPips: 20,
dailyLossLimitPercent: 3,
cooldownSeconds: 15,
maxSpreadPips: 30
};
setupSessions.set(chatId, session);
await ctx.reply(
"🤖 *SETUP AUTO TRADING — Step 1 / 5*\n\nPilih jenis akun yang ingin digunakan:",
setupAccountKeyboard()
);
});
bot.action(/^market:alert:(.+)$/, async (ctx) => {
await ctx.answerCbQuery();
const symbolCode = ctx.match[1];
await ctx.reply(`🔔 Price Alert untuk ${symbolCode} berhasil dibuat pada level harga saat ini.`, mainMenuKeyboard());
});
// ─── Setup Auto Trading Wizards ──────────────────────────────────────────
bot.action("auto:setup", async (ctx) => {
await ctx.answerCbQuery();
const chatId = ctx.chat?.id ?? 0;
const session: AutoTradingSetupSession = {
step: "account",
accountMode: "demo",
symbolCode: "XAUUSD",
strategy: "fast_scalping",
lot: 0.01,
maxPosition: 1,
targetProfitPips: 15,
stopLossPips: 20,
dailyLossLimitPercent: 3,
cooldownSeconds: 15,
maxSpreadPips: 30
};
setupSessions.set(chatId, session);
await ctx.reply(
"🤖 *SETUP AUTO TRADING — Step 1 / 5*\n\nPilih jenis akun yang ingin digunakan:",
setupAccountKeyboard()
);
});
bot.action(/^setup:account:(demo|real)$/, async (ctx) => {
await ctx.answerCbQuery();
const chatId = ctx.chat?.id ?? 0;
const session = setupSessions.get(chatId);
if (!session) return;
session.accountMode = ctx.match[1] as "demo" | "real";
session.step = "market";
await ctx.reply(
"🤖 *SETUP AUTO TRADING — Step 2 / 5*\n\nPilih market yang ingin ditradingkan:",
setupMarketKeyboard()
);
});
bot.action(/^setup:market:(.+)$/, async (ctx) => {
await ctx.answerCbQuery();
const chatId = ctx.chat?.id ?? 0;
const session = setupSessions.get(chatId);
if (!session) return;
const marketChoice = ctx.match[1];
if (marketChoice === "watchlist") {
const watchlist = await tradingService.getWatchlist(chatId);
if (watchlist.length === 0) {
await ctx.reply("❌ Watchlist Anda kosong. Pilih salah satu market utama di bawah:");
return;
}
session.symbolCode = watchlist[0];
} else {
session.symbolCode = marketChoice;
}
session.step = "strategy";
await ctx.reply(
"🤖 *SETUP AUTO TRADING — Step 3 / 5*\n\nPilih strategi trading yang ingin digunakan:",
setupStrategyKeyboard()
);
});
bot.action(/^setup:strategy:(.+)$/, async (ctx) => {
await ctx.answerCbQuery();
const chatId = ctx.chat?.id ?? 0;
const session = setupSessions.get(chatId);
if (!session) return;
session.strategy = ctx.match[1] as any;
session.step = "risk";
await ctx.reply(formatSetupRisk(session), setupRiskKeyboard());
});
// Edit risk parameters
bot.action(/^setup:risk:(lot|tp|sl|dailyloss|cooldown)$/, async (ctx) => {
await ctx.answerCbQuery();
const chatId = ctx.chat?.id ?? 0;
const session = setupSessions.get(chatId);
if (!session) return;
const param = ctx.match[1];
session.waitingInput = param as any;
const prompts: Record<string, string> = {
lot: "Lot (contoh: 0.02)",
tp: "Target Profit dalam Pips (contoh: 15)",
sl: "Stop Loss dalam Pips (contoh: 20)",
dailyloss: "Daily Loss Limit dalam % (contoh: 5)",
cooldown: "Cooldown setelah close dalam detik (contoh: 30)"
};
await ctx.reply(`✍️ Silakan ketik nilai baru untuk *${prompts[param]}*:`);
});
bot.action("setup:risk:next", async (ctx) => {
await ctx.answerCbQuery();
const chatId = ctx.chat?.id ?? 0;
const session = setupSessions.get(chatId);
if (!session) return;
session.step = "review";
await ctx.reply(formatSetupReview({
accountMode: session.accountMode,
marketSymbol: session.symbolCode,
strategy: session.strategy,
lot: session.lot,
maxPosition: session.maxPosition,
targetProfitPips: session.targetProfitPips,
stopLossPips: session.stopLossPips,
dailyLossLimitPercent: session.dailyLossLimitPercent,
maxSpreadPips: session.maxSpreadPips,
cooldownSeconds: session.cooldownSeconds
}), setupReviewKeyboard());
});
bot.action("setup:review:edit", async (ctx) => {
await ctx.answerCbQuery();
const chatId = ctx.chat?.id ?? 0;
const session = setupSessions.get(chatId);
if (!session) return;
session.step = "risk";
await ctx.reply(formatSetupRisk(session), setupRiskKeyboard());
});
bot.action("setup:review:start", async (ctx) => {
await ctx.answerCbQuery();
const chatId = ctx.chat?.id ?? 0;
const session = setupSessions.get(chatId);
if (!session) return;
if (session.accountMode === "real") {
await ctx.reply(formatRealConfirmation(), setupRealConfirmKeyboard());
} else {
await startAutoTradingBot(ctx, autoTradingService, session);
}
});
bot.action("setup:confirm:start", async (ctx) => {
await ctx.answerCbQuery();
const chatId = ctx.chat?.id ?? 0;
const session = setupSessions.get(chatId);
if (!session) return;
await startAutoTradingBot(ctx, autoTradingService, session);
});
// Setup Back Handlers
bot.action("setup:back:market", async (ctx) => {
await ctx.answerCbQuery();
await ctx.reply(
"🤖 *SETUP AUTO TRADING — Step 1 / 5*\n\nPilih jenis akun yang ingin digunakan:",
setupAccountKeyboard()
);
});
bot.action("setup:back:strategy", async (ctx) => {
await ctx.answerCbQuery();
await ctx.reply(
"🤖 *SETUP AUTO TRADING — Step 2 / 5*\n\nPilih market yang ingin ditradingkan:",
setupMarketKeyboard()
);
});
bot.action("setup:back:risk", async (ctx) => {
await ctx.answerCbQuery();
await ctx.reply(
"🤖 *SETUP AUTO TRADING — Step 3 / 5*\n\nPilih strategi trading yang ingin digunakan:",
setupStrategyKeyboard()
);
});
// ─── Status, History & Emergency Stop ─────────────────────────────────────
bot.action("auto:status", async (ctx) => {
await ctx.answerCbQuery();
const isEnabled = autoTradingService?.isEnabled() ?? false;
await ctx.reply(
buildAutoTradingMessage(autoTradingService),
autoTradingDashboardKeyboard(isEnabled)
);
});
bot.action("auto:history", async (ctx) => {
await ctx.answerCbQuery();
const chatId = ctx.chat?.id ?? 0;
try {
const todayTrades = await (tradingService as any).store.listTodayPaperTrades(chatId);
if (todayTrades.length === 0) {
await ctx.reply("📜 Belum ada riwayat transaksi tercatat hari ini.", mainMenuKeyboard());
} else {
const lines = ["📜 *RIWAYAT TRANSAKSI HARI INI*", ""];
for (const t of todayTrades.slice(-10)) {
const emoji = t.pnl !== undefined ? (t.pnl >= 0 ? "🟢" : "🔴") : "⚪";
lines.push(
`${emoji} *${t.symbolCode} ${t.action}* @ ${t.entry} | P&L: \`${
t.pnl !== undefined ? (t.pnl >= 0 ? "+" : "") + t.pnl.toFixed(2) : "floating"
}\``
);
}
await ctx.reply(lines.join("\n"), mainMenuKeyboard());
}
} catch (e) {
await ctx.reply("Error: Gagal memuat riwayat transaksi.", mainMenuKeyboard());
}
});
bot.action("auto:stop", async (ctx) => {
await ctx.answerCbQuery();
if (autoTradingService) {
const closedCount = await autoTradingService.emergencyStop();
await ctx.reply(
`🛑 *EMERGENCY STOP TELAH DIAKTIFKAN!*\n\nStatus robot sekarang: 🔴 OFF.\nBerhasil menutup paksa \`${closedCount}\` posisi aktif di MT5.`,
mainMenuKeyboard()
);
} else {
await ctx.reply("Error: Auto Trading service tidak tersedia.", mainMenuKeyboard());
}
});
// ─── Chat Text Input Processing (NLP & Parameter Input) ───────────────────
bot.on("text", async (ctx) => {
if (!isAllowedUser(ctx.from?.id)) {
await ctx.reply("Akses bot tidak diizinkan untuk user ini.");
return;
}
const chatId = ctx.chat.id;
const session = setupSessions.get(chatId);
// Tangani input teks untuk setup manajemen risiko jika sedang menunggu input
if (session && session.waitingInput) {
const value = parseFloat(ctx.message.text);
if (isNaN(value) || value <= 0) {
await ctx.reply("❌ Input tidak valid. Harap masukkan angka positif yang valid.");
return;
}
switch (session.waitingInput) {
case "lot":
session.lot = value;
break;
case "tp":
session.targetProfitPips = value;
break;
case "sl":
session.stopLossPips = value;
break;
case "dailyloss":
session.dailyLossLimitPercent = value;
break;
case "cooldown":
session.cooldownSeconds = Math.round(value);
break;
}
session.waitingInput = undefined;
await ctx.reply(formatSetupRisk(session), setupRiskKeyboard());
return;
}
// Alur chat bebas NLP standard
const symbol = detectMarketSymbolFromText(ctx.message.text);
if (!symbol) {
await ctx.reply("Saya belum menemukan pair. Contoh: XAUUSD buy atau sell, EURUSD intraday.", mainMenuKeyboard());
return;
}
await sendMarketAnalysisScreen(ctx, tradingService, symbol.code);
});
}
// ─── Helper Functions ────────────────────────────────────────────────────────
async function sendMarketAnalysisScreen(
ctx: Context,
tradingService: TradingService,
symbolCode: string
): Promise<void> {
const symbol = findMarketSymbol(symbolCode);
if (!symbol) {
await ctx.reply("Symbol market tidak didukung.", mainMenuKeyboard());
return;
}
try {
let currentPrice = 0;
let signalAction = "WAIT";
let volatility = "Medium";
try {
const signalResult = await tradingService.createSignal({
chatId: ctx.chat?.id ?? 0,
userId: ctx.from?.id ?? 0,
symbolCode: symbol.code,
mode: "intraday" // default mode untuk preview analisa
});
currentPrice = signalResult.signal.currentPrice;
signalAction = signalResult.signal.action;
const volatilityPercent = signalResult.signal.snapshot.volatilityPercent;
volatility = volatilityPercent > 1.2 ? "High" : volatilityPercent > 0.7 ? "Medium" : "Low";
} catch {
currentPrice = symbol.code === "XAUUSD" ? 2360 : symbol.code === "BTCUSD" ? 60000 : 1.085;
}
// Ambil tren secara dinamis untuk multi-timeframe
const trendM1 = await getTrendForTimeframe(tradingService, symbol, "M1");
const trendM5 = await getTrendForTimeframe(tradingService, symbol, "M5");
const trendM15 = await getTrendForTimeframe(tradingService, symbol, "M15");
const trendH1 = await getTrendForTimeframe(tradingService, symbol, "H1");
const spreadPoints = Math.round(symbol.defaultSpreadPips * 10);
const message = formatMarketAnalysis({
symbolLabel: symbol.label,
currentPrice,
action: signalAction,
trendM1,
trendM5,
trendM15,
trendH1,
spreadPoints,
volatility
});
await ctx.reply(message, marketAnalysisKeyboard(symbol.code));
} catch (error) {
await ctx.reply(safeError(error), mainMenuKeyboard());
}
}
async function getTrendForTimeframe(
tradingService: TradingService,
symbol: MarketSymbol,
timeframe: "M1" | "M5" | "M15" | "H1"
): Promise<string> {
try {
const marketData = await (tradingService as any).marketDataService.getCandles(symbol, timeframe, 30);
const candles = marketData.candles;
if (candles.length < 2) return "Sideways";
const lastPrice = candles.at(-1)!.close;
const closes = candles.map((c: any) => c.close);
const ema20 = exponentialMovingAverage(closes, 20);
return lastPrice >= ema20 ? "Bullish" : "Bearish";
} catch {
const bias = getStableBias(symbol.code, timeframe);
return bias >= 0 ? "Bullish" : "Bearish";
}
}
function getStableBias(symbolCode: string, timeframe: string): number {
const value = `${symbolCode}-${timeframe}`
.split("")
.reduce((sum, character) => sum + character.charCodeAt(0), 0);
return (value % 7) - 3;
}
async function startAutoTradingBot(
ctx: Context,
autoTradingService: AutoTradingService | undefined,
session: AutoTradingSetupSession
): Promise<void> {
if (!autoTradingService) {
await ctx.reply("❌ Error: Auto Trading service tidak tersedia.", mainMenuKeyboard());
return;
}
const chatId = ctx.chat?.id ?? 0;
// Konfigurasikan robot dengan parameter dynamic hasil setup
autoTradingService.configure({
enabled: true,
symbols: [session.symbolCode],
mode: session.strategy === "fast_scalping" ? "scalping" : "intraday",
cooldownSeconds: session.cooldownSeconds,
maxOpenPositions: session.maxPosition,
maxSpreadPips: session.maxSpreadPips,
chatId: chatId,
userId: ctx.from?.id ?? 0,
accountMode: session.accountMode,
strategy: session.strategy,
lot: session.lot,
targetProfitPips: session.targetProfitPips,
stopLossPips: session.stopLossPips,
dailyLossLimitPercent: session.dailyLossLimitPercent
});
const started = autoTradingService.start(true);
if (started) {
const accType = session.accountMode === "real" ? "REAL 💰" : "DEMO 🧪";
await ctx.reply(
`🟢 *Auto Trading telah diaktifkan!*\n\n• Akun: *${accType}*\n• Market: *${session.symbolCode}*\n• Strategi: *${session.strategy}*\n• Lot Size: \`${session.lot.toFixed(2)}\``,
mainMenuKeyboard()
);
setupSessions.delete(chatId);
} else {
await ctx.reply("❌ Gagal mengaktifkan Auto Trading. Periksa kembali status broker.", mainMenuKeyboard());
}
}
function isAllowedUser(userId: number | undefined): boolean {
if (env.allowedTelegramUserIds.size === 0) {
return true;
}
return Boolean(userId && env.allowedTelegramUserIds.has(userId));
}
async function buildSettingsMessage(tradingService: TradingService): Promise<string> {
let brokerStatus: BrokerExecutionStatus | undefined;
try {
brokerStatus = await tradingService.getBrokerExecutionStatus();
} catch {
brokerStatus = undefined;
}
return formatSettings({
runtimeMode: env.tradingMode,
enableRealTrading: env.enableRealTrading,
accountBalance: env.risk.accountBalance,
riskPerTradePercent: env.risk.riskPerTradePercent,
maxDailyLossPercent: env.risk.maxDailyLossPercent,
maxOpenTrades: env.risk.maxOpenTrades,
maxLotPerOrder: env.risk.maxLotPerOrder,
marketDataProvider: env.marketDataProvider,
brokerProvider: env.brokerProvider,
autoTradingEnabled: env.autoTrading.enabled,
brokerStatus
});
}
function buildAutoTradingMessage(autoTradingService: AutoTradingService | undefined): string {
if (!autoTradingService) {
return formatAutoTradingDashboard({ isEnabled: false });
}
const isEnabled = autoTradingService.isEnabled();
const config = (autoTradingService as any).config;
return formatAutoTradingDashboard({
isEnabled,
accountMode: config.accountMode,
marketSymbol: config.symbols?.[0],
strategy: config.strategy,
lot: config.lot
});
}
function safeError(error: unknown): string {
if (error instanceof Error) {
return `Error: ${error.message}`;
}
return "Error tidak dikenal.";
}