π νκ΅μ΄λ‘ 보기
Toss Auto-Trading Dev Journal (Phase 5 / 16 posts so far)
- #0 Foundation
- #3 Config split & retry logic
- #7 Strategy class
- #9 Data persistence design
- #13 Backtest engine
- #15 Read-only dashboard
- #16 Telegram bot command handlers (this post)
π‘ Phase 5-2 summary
- Goal: connect python-telegram-bot so /status, /holdings, /pause, /resume actually work from Telegram
- Key decision: the bot server only handles transport β it calls the same pure functions from bot_commands.py / engine_control.py already built
- Verification: 7 new unit tests with mocked Update objects, full suite at 109/109 passing, then a live /status check in the actual Telegram app
The logic layer (engine_control.py, bot_commands.py) was already done from a prior session. This time: actually wiring it to Telegram.
Testing what you can before you even have a bot token
Before getting a token from BotFather, the handler logic itself is testable without one. So I ran both tracks in parallel.
bot_server.py β async handlers on python-telegram-bot v22
Version 22 moved to a fully async API, so handlers get registered like this:
from telegram import Update
from telegram.ext import ApplicationBuilder, CommandHandler, ContextTypes
async def status_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not is_authorized(update.effective_chat.id):
await update.message.reply_text("Unauthorized")
return
text = build_status_text()
await update.message.reply_text(text)
app = ApplicationBuilder().token(TELEGRAM_BOT_TOKEN).build()
app.add_handler(CommandHandler("status", status_handler))
A security gap found mid-build β access control
Anyone who knows the bot’s username can message it. Fixed immediately by checking the incoming chat ID against TELEGRAM_CHAT_ID in .env.
def is_authorized(chat_id: int) -> bool:
return str(chat_id) == os.getenv("TELEGRAM_CHAT_ID")
Testing with mocked Update objects
No live Telegram server needed β mock Update/Message objects let me test the handlers in isolation: unauthorized rejection, actual state changes from /pause and /resume, auth failure/success paths for /holdings.
Full pytest run: 109/109 passing (102 existing + 7 new).
Real-world check β the empty chat ID mystery
Querying Telegram’s getUpdates API for the chat ID first returned "result": []. Turned out I’d never sent a message in that chat β hitting “Start” and sending anything fixed it immediately.
With the token and chat ID in .env, running python bot_server.py and sending /status from the actual Telegram app returned the correct response.
Registering async CommandHandlers on python-telegram-bot v22 and checking the incoming chat ID against an env-stored whitelist gives a solo trading bot safe remote control over Telegram.
FAQ
Q1. What changed in python-telegram-bot v22?
It moved to a fully async (async/await) API β handlers must be coroutines.
Q2. Can anyone message my bot if they know the username?
Yes by default β which is why a chat-ID whitelist check is required.
Q3. How do I find my chat ID?
Send a message in the chat first, then query getUpdates β an empty chat won’t return anything.
Q4. Can I test without a live Telegram server?
Yes β mock Update/Message objects isolate the handler logic for testing.