Bot & Automation

Market AI

/root/hermes-projects/Market AI

packages/core/src/formatters.ts text
import { BOT_NAME, MEME_MENU } from "./constants.js";
import type { MemeCoinCandidate, MemeSignalAnalysis, TradeExecutionResult, TradeQuote } from "./types.js";

export function escapeHtml(value: string): string {
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
}

export function formatMainMenuCard(): string {
  return [
    `<b>${BOT_NAME}</b>`,
    "<i>Scanner dan trading assistant khusus meme coin. Default aman: paper trading sampai live executor dikonfigurasi.</i>",
    "",
    "<b>Menu:</b>",
    `- ${MEME_MENU.scanner}`,
    `- ${MEME_MENU.topPicks}`,
    `- ${MEME_MENU.analyze}`,
    `- ${MEME_MENU.buy} / ${MEME_MENU.sell}`,
    `- ${MEME_MENU.positions}`,
    `- ${MEME_MENU.watchlist}`,
    `- ${MEME_MENU.alerts}`,
    "",
    "<b>Contoh:</b>",
    "<code>/scan solana</code>",
    "<code>/analyze BONK</code>",
    "<code>/buy TOKEN_ADDRESS 0.05</code>",
    "<code>/sell TOKEN_ADDRESS 25</code>",
    "",
    "<i>Bot tidak menjamin profit. Semua live trade butuh konfirmasi dan env executor.</i>",
  ].join("\n");
}

export function formatCandidateLine(candidate: MemeCoinCandidate, index: number): string {
  const token = candidate.token;
  const metrics = candidate.metrics;
  return [
    `<b>${index}. ${escapeHtml(token.symbol)}</b> ${escapeHtml(token.name)}`,
    `Score ${candidate.score}/100 | ${candidate.verdict} | Risk ${candidate.riskLevel}`,
    `Chain ${escapeHtml(token.chainId)} | Liq $${Math.round(metrics.liquidityUsd).toLocaleString("en-US")} | Vol24 $${Math.round(metrics.volumeH24).toLocaleString("en-US")}`,
    `H1 ${metrics.priceChangeH1 ?? 0}% | H24 ${metrics.priceChangeH24 ?? 0}% | Buys/Sells ${metrics.buysH24}/${metrics.sellsH24}`,
    token.url ? `<a href="${escapeHtml(token.url)}">DexScreener</a>` : "",
  ]
    .filter(Boolean)
    .join("\n");
}

export function formatScannerResults(candidates: MemeCoinCandidate[]): string {
  if (candidates.length === 0) {
    return [
      "<b>Scanner Result</b>",
      "",
      "Belum ada kandidat yang lolos filter saat ini.",
      "Turunkan filter atau scan lagi nanti saat volume market meningkat.",
    ].join("\n");
  }

  return [
    "<b>Meme Coin Scanner</b>",
    "<i>Kandidat diurutkan berdasarkan liquidity, volume, momentum, buy/sell pressure, dan penalti risiko.</i>",
    "",
    ...candidates.flatMap((candidate, index) => [formatCandidateLine(candidate, index + 1), ""]),
  ].join("\n");
}

export function formatMemeSignalCard(signal: MemeSignalAnalysis): string {
  const { candidate } = signal;

  return [
    "<b>MEME COIN ANALYSIS</b>",
    "",
    `<b>Token</b>: ${escapeHtml(candidate.token.symbol)} - ${escapeHtml(candidate.token.name)}`,
    `<b>Chain</b>: ${escapeHtml(candidate.token.chainId)}`,
    `<b>Address</b>: <code>${escapeHtml(candidate.token.tokenAddress)}</code>`,
    candidate.token.url ? `<b>Chart</b>: <a href="${escapeHtml(candidate.token.url)}">DexScreener</a>` : "",
    "",
    `<b>Action</b>: ${signal.action}`,
    `<b>Trend</b>: ${signal.trend}`,
    `<b>Confidence</b>: ${signal.confidence}%`,
    `<b>Scanner Score</b>: ${candidate.score}/100`,
    `<b>Verdict</b>: ${candidate.verdict}`,
    `<b>Risk</b>: ${signal.riskLevel}`,
    `<b>Max Position Risk</b>: ${escapeHtml(signal.maxPositionSize)}`,
    "",
    "<b>Market Data</b>",
    `Price: $${candidate.metrics.priceUsd}`,
    `Liquidity: $${Math.round(candidate.metrics.liquidityUsd).toLocaleString("en-US")}`,
    `Volume 24h: $${Math.round(candidate.metrics.volumeH24).toLocaleString("en-US")}`,
    `FDV: ${candidate.metrics.fdv ? `$${Math.round(candidate.metrics.fdv).toLocaleString("en-US")}` : "N/A"}`,
    `H1/H24: ${candidate.metrics.priceChangeH1 ?? 0}% / ${candidate.metrics.priceChangeH24 ?? 0}%`,
    `Buys/Sells 24h: ${candidate.metrics.buysH24}/${candidate.metrics.sellsH24}`,
    "",
    "<b>Plan</b>",
    `Entry: ${escapeHtml(signal.entryPlan)}`,
    `Stop: ${escapeHtml(signal.stopLossPlan)}`,
    `TP: ${escapeHtml(signal.takeProfitPlan)}`,
    "",
    "<b>Reason</b>",
    ...signal.reasons.map((reason) => `- ${escapeHtml(reason)}`),
    signal.warnings.length ? "" : "",
    signal.warnings.length ? "<b>Warnings</b>" : "",
    ...signal.warnings.map((warning) => `- ${escapeHtml(warning)}`),
    "",
    `<b>Invalidation</b>: ${escapeHtml(signal.invalidation)}`,
    "",
    `<i>${escapeHtml(signal.disclaimer)}</i>`,
  ]
    .filter((line) => line !== "")
    .join("\n");
}

export function formatTradeQuote(quote: TradeQuote): string {
  return [
    "<b>Trade Preview</b>",
    "",
    `<b>Mode</b>: ${quote.request.mode.toUpperCase()}`,
    `<b>Side</b>: ${quote.request.side}`,
    `<b>Chain</b>: ${escapeHtml(quote.request.chainId)}`,
    `<b>Token</b>: <code>${escapeHtml(quote.request.tokenAddress)}</code>`,
    `<b>Amount</b>: ${quote.request.amount}`,
    `<b>Slippage</b>: ${quote.request.slippageBps / 100}%`,
    `<b>Estimated Output</b>: ${escapeHtml(quote.estimatedOutput)} ${escapeHtml(quote.outputSymbol)}`,
    `<b>Route</b>: ${escapeHtml(quote.routeSummary)}`,
    quote.priceImpactPct !== undefined ? `<b>Price Impact</b>: ${quote.priceImpactPct}%` : "",
    "",
    ...quote.warnings.map((warning) => `- ${escapeHtml(warning)}`),
    "",
    "<i>Tekan Confirm hanya jika sudah paham risiko. Live trade membutuhkan executor/wallet yang valid.</i>",
  ]
    .filter(Boolean)
    .join("\n");
}

export function formatTradeResult(result: TradeExecutionResult): string {
  return [
    "<b>Trade Result</b>",
    "",
    `<b>Status</b>: ${result.status}`,
    `<b>Mode</b>: ${result.mode}`,
    result.txHash ? `<b>Tx</b>: <code>${escapeHtml(result.txHash)}</code>` : "",
    escapeHtml(result.message),
  ]
    .filter(Boolean)
    .join("\n");
}

export function formatAlertHelp(): string {
  return [
    "<b>Meme Coin Alert</b>",
    "",
    "Format:",
    "<code>/alert TOKEN_ADDRESS price_above 0.001</code>",
    "<code>/alert TOKEN_ADDRESS score_above 75</code>",
    "",
    "Alert aktif saat worker database-backed rule dihubungkan ke scheduler.",
  ].join("\n");
}