Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions data/runtime-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"PRICE_TARGETS": "BTC:ABOVE:95000,GLD:BELOW:420",
"ETF_SYMBOLS": "GLD,SLV,BNO,SMH"
}
85 changes: 85 additions & 0 deletions src/bot/commandHandlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ const { sendTelegramMessage } = require("../telegram");
const { fetchYahooQuotes } = require("../providers/yahoo");
const { formatPrice, formatPercent, escapeHtml } = require("../utils/formatters");
const { formatRecentEvents } = require("../utils/eventHistory");
const {
getEditableSettings,
getEditableSettingKeys,
setEditableSetting,
resetEditableSetting,
} = require("../runtimeConfig");

async function handleCommand(text, state, handlers) {
const parts = text.trim().split(/\s+/);
Expand Down Expand Up @@ -113,13 +119,92 @@ async function handleCommand(text, state, handlers) {
return;
}

if (command === "/config") {
const settings = getEditableSettings();
const top = settings
.slice(0, 14)
.map(
(item) =>
`• <b>${escapeHtml(item.key)}</b> = <code>${escapeHtml(item.value || "")}</code>${
item.overridden ? " <i>(runtime)</i>" : ""
}`
)
.join("\n");
await sendTelegramMessage(
`<b>⚙️ Runtime Config</b>\n\n` +
`${top}\n\n` +
`<i>Usa /set KEY VALUE para cambiar y /unset KEY para restaurar desde .env.</i>`,
{ parseMode: "HTML" }
);
return;
}

if (command === "/set") {
const match = text.match(/^\/set(?:@\w+)?\s+(\S+)\s+([\s\S]+)$/i);
if (!match) {
await sendTelegramMessage(
`Uso: <b>/set KEY VALUE</b>\nEjemplo: <code>/set ETF_WARNING_THRESHOLD_PERCENT 0.8</code>`,
{ parseMode: "HTML" }
);
return;
}

const envKey = String(match[1] || "").toUpperCase();
const rawValue = String(match[2] || "").trim();

try {
const updated = await setEditableSetting(envKey, rawValue);
await sendTelegramMessage(
`✅ <b>${escapeHtml(updated.key)}</b> actualizado a <code>${escapeHtml(
updated.value
)}</code>\n<i>Cambio aplicado en runtime y persistido.</i>`,
{ parseMode: "HTML" }
);
} catch (error) {
const validKeys = getEditableSettingKeys().slice(0, 18).join(", ");
await sendTelegramMessage(
`⚠️ No se pudo actualizar: ${escapeHtml(error.message)}\n\n` +
`<b>Claves soportadas (resumen):</b>\n<code>${escapeHtml(validKeys)}</code>`,
{ parseMode: "HTML" }
);
}
return;
}

if (command === "/unset") {
if (!arg) {
await sendTelegramMessage(
`Uso: <b>/unset KEY</b>\nEjemplo: <code>/unset ETF_WARNING_THRESHOLD_PERCENT</code>`,
{ parseMode: "HTML" }
);
return;
}

const envKey = String(arg || "").toUpperCase();
try {
const updated = await resetEditableSetting(envKey);
await sendTelegramMessage(
`↩️ <b>${escapeHtml(updated.key)}</b> restaurado a <code>${escapeHtml(
updated.value
)}</code> desde .env`,
{ parseMode: "HTML" }
);
} catch (error) {
await sendTelegramMessage(`⚠️ ${escapeHtml(error.message)}`, { parseMode: "HTML" });
}
return;
}

if (command === "/ayuda" || command === "/help") {
await sendTelegramMessage(
`<b>🤖 Market Watcher — Commands</b>\n\n` +
`/report — Full dashboard right now\n` +
`/price SYMBOL — Current price for an asset\n` +
`/status — Bot status and last check\n` +
`/events — Recent alert and schedule history\n` +
`/config — Show current runtime config\n` +
`/set KEY VALUE — Update runtime config\n` +
`/unset KEY — Restore value from .env\n` +
`/help — This help\n\n` +
`<i>Examples: /price BTC /price GLD /price AAPL</i>`,
{ parseMode: "HTML" }
Expand Down
2 changes: 1 addition & 1 deletion src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ const config = {
apiRetryMaxDelayMs: parseNumber(process.env.API_RETRY_MAX_DELAY_MS, 4000),
requestTimeoutMs: 15000,
webEnabled: (process.env.WEB_ENABLED || "true").toLowerCase() !== "false",
webPort: parseNumber(process.env.WEB_PORT, 1903),
webPort: parseNumber(process.env.WEB_PORT, 1904),
maxCsvSizeMb: parseNumber(process.env.MAX_CSV_SIZE_MB, 20),
};

Expand Down
14 changes: 10 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const { runCycle, buildDailyReport, fetchAllQuotes } = require("./marketMonitor"
const { sendTelegramMessage } = require("./telegram");
const { pollAndHandle } = require("./telegramCommands");
const { startWebServer } = require("./webServer");
const { loadRuntimeConfig } = require("./runtimeConfig");
const { logger } = require("./utils/logger");

let cycleRunning = false;
Expand Down Expand Up @@ -47,6 +48,7 @@ async function safeRunCycle(state, options) {
}

async function main() {
await loadRuntimeConfig();
validateConfig();
const state = await loadState();

Expand All @@ -57,19 +59,23 @@ async function main() {
logger.info("Starting market monitor...");
await safeRunCycle(state, { isStartup: true });

setInterval(async () => {
const runCycleLoop = async () => {
await safeRunCycle(state, { isStartup: false });
}, config.checkIntervalMs);
setTimeout(runCycleLoop, config.checkIntervalMs);
};
setTimeout(runCycleLoop, config.checkIntervalMs);

// Telegram command polling (/report, /price, /status, /help)
const commandHandlers = { buildDailyReport, fetchAllQuotes };
setInterval(async () => {
const pollLoop = async () => {
try {
await pollAndHandle(state, commandHandlers);
} catch (error) {
logger.warn(`Telegram polling error: ${error.message}`);
}
}, config.commandPollIntervalMs);
setTimeout(pollLoop, config.commandPollIntervalMs);
};
setTimeout(pollLoop, config.commandPollIntervalMs);

logger.info(
`Background monitor active. Interval: ${Math.round(config.checkIntervalMs / 60000)} minutes.`
Expand Down
Loading
Loading