Skip to content

feat: add Telegram trading bot for Reya exchange#45

Open
0xsmolrun wants to merge 1 commit intoReya-Labs:mainfrom
0xsmolrun:claude/telegram-trading-bot-QQ3Z7
Open

feat: add Telegram trading bot for Reya exchange#45
0xsmolrun wants to merge 1 commit intoReya-Labs:mainfrom
0xsmolrun:claude/telegram-trading-bot-QQ3Z7

Conversation

@0xsmolrun
Copy link

@0xsmolrun 0xsmolrun commented Mar 6, 2026

Implements a fully-featured Telegram bot that wraps the Reya Python SDK, allowing users to trade perpetuals and spot markets directly from Telegram.

Bot commands:

  • /prices, /price , /markets, /symbols — market data
  • /accounts, /balance, /positions, /orders, /history — account info
  • /buy, /sell — IOC limit orders
  • /buygtc, /sellgtc — GTC limit orders
  • /sl, /tp — stop-loss and take-profit trigger orders
  • /cancel <ORDER_ID> — cancel an order

Files added:
telegram_bot/init.py — package marker
telegram_bot/main.py — entry point, env loading, access control
telegram_bot/bot.py — Telegram Application + all command handlers
telegram_bot/trading.py — async TradingService wrapping ReyaTradingClient
telegram_bot/formatters.py — Markdown message formatters

Also:
pyproject.toml — added python-telegram-bot>=21.0 dependency and package
.env.example — added TELEGRAM_BOT_TOKEN and ALLOWED_USER_IDS vars

Run with: python -m telegram_bot.main

https://claude.ai/code/session_01YGfEGdKJQXu8fQHati5XWX

Summary by CodeRabbit

Release Notes

  • New Features
    • Telegram bot interface for trading on Reya exchange with optional user access control
    • Market data commands: view prices, accounts, balances, positions, open orders, and execution history
    • Trading commands: place immediate and limit orders, set stop-loss/take-profit orders, and cancel orders
    • Markdown-formatted messages for enhanced readability

Implements a fully-featured Telegram bot that wraps the Reya Python SDK,
allowing users to trade perpetuals and spot markets directly from Telegram.

Bot commands:
- /prices, /price <SYMBOL>, /markets, /symbols — market data
- /accounts, /balance, /positions, /orders, /history — account info
- /buy, /sell — IOC limit orders
- /buygtc, /sellgtc — GTC limit orders
- /sl, /tp — stop-loss and take-profit trigger orders
- /cancel <ORDER_ID> — cancel an order

Files added:
  telegram_bot/__init__.py   — package marker
  telegram_bot/main.py       — entry point, env loading, access control
  telegram_bot/bot.py        — Telegram Application + all command handlers
  telegram_bot/trading.py    — async TradingService wrapping ReyaTradingClient
  telegram_bot/formatters.py — Markdown message formatters

Also:
  pyproject.toml  — added python-telegram-bot>=21.0 dependency and package
  .env.example    — added TELEGRAM_BOT_TOKEN and ALLOWED_USER_IDS vars

Run with: python -m telegram_bot.main

https://claude.ai/code/session_01YGfEGdKJQXu8fQHati5XWX
@coderabbitai
Copy link

coderabbitai bot commented Mar 6, 2026

📝 Walkthrough

Walkthrough

The changes introduce a Telegram bot module for Reya trading. It includes environment configuration, a TradingService wrapper for the trading SDK, command handlers for market data and order management, message formatters for Telegram output, and a main entrypoint with optional user access control.

Changes

Cohort / File(s) Summary
Configuration & Dependencies
.env.example, pyproject.toml
Added Telegram bot token and allowed user IDs environment variables; added python-telegram-bot>=21.0,<22.0 dependency and telegram_bot package to Poetry configuration.
Bot Package Core
telegram_bot/__init__.py, telegram_bot/bot.py
Created telegram_bot package with module docstring; implemented asynchronous command-driven bot with handlers for market data queries, account operations, order management (IOC, GTC, trigger orders), and cancellation; includes argument parsing, error handling, and reply helper with Markdown fallback.
Trading Service
telegram_bot/trading.py
Introduced TradingService class wrapping ReyaTradingClient with lifecycle management (start/stop); exposes async methods for market data retrieval, account/wallet access, and order operations (buy/sell IOC/GTC, stop-loss, take-profit, cancellation).
Message Formatting
telegram_bot/formatters.py
Added 10 formatter functions generating Markdown-formatted Telegram messages for prices, accounts, balances, positions, orders, executions, and market summaries; includes price conversion helper and fallback handling for missing data.
Application Entrypoint
telegram_bot/main.py
Implemented main entrypoint loading environment variables, initializing TradingConfig and TradingService, building Telegram Application, and applying optional access control; includes graceful shutdown and error handling for missing configuration.

Sequence Diagram

sequenceDiagram
    actor User as Telegram User
    participant Bot as Telegram Bot
    participant Handler as Command Handler
    participant Service as TradingService
    participant Client as ReyaTradingClient
    participant Formatter as Message Formatter
    
    User->>Bot: /prices command
    activate Bot
    Bot->>Handler: route command
    activate Handler
    Handler->>Service: get_prices()
    activate Service
    Service->>Client: fetch prices
    activate Client
    Client-->>Service: prices list
    deactivate Client
    deactivate Service
    Service-->>Handler: prices list
    Handler->>Formatter: fmt_prices(prices)
    activate Formatter
    Formatter-->>Handler: markdown string
    deactivate Formatter
    Handler->>Bot: send formatted message
    deactivate Handler
    Bot-->>User: market prices display
    deactivate Bot
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰✨ A rabbit's delight in code so bright,
Telegram speaks to traders day and night,
Commands and orders, formatted with care,
The market whispers through the data air!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add Telegram trading bot for Reya exchange' directly and accurately summarizes the primary change: adding a complete Telegram bot module for trading on the Reya exchange, which is the core focus of all files added in this PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pyproject.toml`:
- Around line 38-39: Remove the "python-telegram-bot>=21.0,<22.0" entry from the
core dependencies list and add it as an optional extra so only consumers who opt
into the bot feature install it; specifically, locate the dependency string
"python-telegram-bot>=21.0,<22.0" in pyproject.toml and delete it from the main
dependencies, then add it under the PEP-621 optional dependencies section (e.g.,
add an entry under [project.optional-dependencies] with a key like "telegram"
whose value is ["python-telegram-bot>=21.0,<22.0"]); ensure the extras key name
("telegram") matches any docs or usage in the repo that reference installing the
telegram feature.

In `@telegram_bot/bot.py`:
- Around line 211-220: The _parse_order_args function currently only attempts
float(qty)/float(price) which allows zero, negatives, NaN, and infinite values;
update _parse_order_args to convert qty and price to floats, then reject values
that are <= 0 or not finite (use math.isfinite), raising ValueError with the
same usage/help text if validation fails; keep returning symbol, qty, price
(either as validated floats or original strings per the surrounding API
expectations) and apply the same finite/positive checks to the other
order-parsing/validation block mentioned in the review.
- Around line 39-45: The help text mislabels IOC orders as “fill-or-kill” (FOK);
update the trading help block that lists the /buy, /sell, /buygtc and /sellgtc
command descriptions to correctly describe IOC as "Immediate-or-Cancel (IOC) —
executes immediately and cancels any unfilled portion (partial fills allowed)"
and keep GTC described as resting limit orders; locate the help text string in
bot.py (the block that contains "*Trading — Perp / Spot IOC (fill-or-kill)*" and
replace the wording accordingly so the /buy and /sell entries reference IOC
semantics rather than FOK.
- Around line 10-12: The _reply helper (and the other try/except at lines
~64-69) currently catches all exceptions; change them to catch only
telegram.error.BadRequest and then inspect the exception message for
parse-related indicators (e.g., "can't parse entities", "Can't parse", or
similar parse/markdown/html errors) before falling back to plain text; for any
other BadRequest messages or any other exception types re-raise the exception so
network/Telegram API errors are not swallowed. Ensure you import BadRequest from
telegram.error and update both exception blocks (the _reply function and the
second handler around lines 64-69) to follow this narrow-catch-and-inspect
pattern.

In `@telegram_bot/formatters.py`:
- Around line 147-153: The loop in summaries uses non-existent attributes
last_price and volume_24h on MarketSummary; replace them with the actual model
fields throttled_oracle_price and volume24h (e.g., get throttled_oracle_price
into last_raw and pass it to _oracle_price_usd, and get volume24h into
volume_raw and format as before), using getattr(s, "throttled_oracle_price",
None) and getattr(s, "volume24h", None) so the `/markets` output shows real
values.
- Around line 89-103: The loop that formats orders uses getattr(o, "is_buy",
True) causing SDK Order objects (which expose side, not is_buy) to be
mis-labeled; update the logic in the orders formatting block to prefer
getattr(o, "side", None) and map its string (e.g., "buy"/"Buy" or "sell"/"Sell")
to the display "Buy"/"Sell", falling back to using getattr(o, "is_buy", None)
(mapping True→"Buy", False→"Sell") only if side is missing, and use a safe
default like "N/A" if neither is present; adjust the reference in the
lines.append construction so the variable used for display is the normalized
side string instead of the current side expression.
- Around line 68-80: fmt_positions is reading non-existent SDK fields (is_long,
size, unrealized_pnl); update it to use the SDK model's actual attributes: use
pos.side (map to "Long"/"Short" as appropriate) instead of is_long, use pos.qty
for size display (format pos.qty / 1e18) instead of size, and use
pos.avg_entry_price for entry price display; remove or fallback to "N/A" for
unrealized_pnl (or use any available pnl field from the SDK if present) so the
formatter shows real data from the SDK model rather than default values.

In `@telegram_bot/main.py`:
- Around line 78-81: The code currently logs a warning when ALLOWED_USER_IDS is
unset, which leaves the bot open; change the behavior to fail-closed by
requiring ALLOWED_USER_IDS unless an explicit opt-in flag (e.g.,
TELEGRAM_PUBLIC_MODE=true) is set. Update the check around allowed_user_ids in
main.py (the variable allowed_user_ids and the logger calls) to: if
allowed_user_ids is empty and TELEGRAM_PUBLIC_MODE is not truthy, log an error
and exit (or raise SystemExit); if TELEGRAM_PUBLIC_MODE is set, keep the
existing info/warning behavior. Apply the same change to the other occurrence
referenced (lines 95-96) so both checks enforce the fail-closed policy.
- Around line 126-139: The _check_user handler currently raises a generic
Exception to stop processing unauthorized updates; change this to raise
telegram.ext.ApplicationHandlerStop (import ApplicationHandlerStop) so PTB
treats it as normal short-circuit control flow rather than an error, and keep
the existing reply_text call before raising ApplicationHandlerStop in
_check_user (or simply return after sending the reply if you prefer to avoid
raising); update the import list to include ApplicationHandlerStop and remove
the generic Exception usage in the TypeHandler(Update, _check_user) flow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 04ac8674-4eac-4e1a-aa7c-3bc2ad28729b

📥 Commits

Reviewing files that changed from the base of the PR and between e889b9c and dd6f022.

📒 Files selected for processing (7)
  • .env.example
  • pyproject.toml
  • telegram_bot/__init__.py
  • telegram_bot/bot.py
  • telegram_bot/formatters.py
  • telegram_bot/main.py
  • telegram_bot/trading.py

Comment on lines +38 to +39
"aiohttp-retry>=2.8.3,<3.0.0",
"python-telegram-bot>=21.0,<22.0"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Read pyproject.toml to check the dependencies and optional-dependencies sections
cat -n pyproject.toml | head -100

Repository: Reya-Labs/reya-python-sdk

Length of output: 3603


🏁 Script executed:

# Find the telegram_bot module mentioned in the comment
find . -type f -name "*telegram*" | head -20

Repository: Reya-Labs/reya-python-sdk

Length of output: 51


🏁 Script executed:

# Check git diff or recent changes to see what was actually added
git diff HEAD~1 pyproject.toml 2>/dev/null || echo "Git diff not available, showing current state"

Repository: Reya-Labs/reya-python-sdk

Length of output: 114


🏁 Script executed:

# Check if telegram_bot directory exists and what it contains
find . -type d -name "telegram_bot" -o -type f -name "telegram_bot*"

Repository: Reya-Labs/reya-python-sdk

Length of output: 83


🏁 Script executed:

# If telegram_bot exists, examine its structure
ls -la telegram_bot/ 2>/dev/null && find telegram_bot -type f | head -20

Repository: Reya-Labs/reya-python-sdk

Length of output: 608


🏁 Script executed:

# Search for any imports of telegram_bot in the main SDK
rg "from telegram_bot|import telegram_bot" --type py

Repository: Reya-Labs/reya-python-sdk

Length of output: 329


🏁 Script executed:

# Also check if python-telegram-bot is imported anywhere in core SDK
rg "from telegram_bot|import telegram" --type py | grep -v "telegram_bot/" | head -20

Repository: Reya-Labs/reya-python-sdk

Length of output: 51


🏁 Script executed:

# Check the main SDK structure and what's exposed in __init__.py
find . -type f -name "__init__.py" | grep -E "^\./(sdk/)?__init__\.py$" | xargs cat -n

Repository: Reya-Labs/reya-python-sdk

Length of output: 51


🏁 Script executed:

# Search for any use of python-telegram-bot package in the SDK core
rg "python.telegram.bot|from telegram|import telegram" sdk/ --type py 2>/dev/null | head -20

Repository: Reya-Labs/reya-python-sdk

Length of output: 51


🏁 Script executed:

# Check if telegram_bot is imported or referenced in the main package __init__
cat -n sdk/__init__.py 2>/dev/null || cat -n __init__.py 2>/dev/null || echo "No main __init__ found"

Repository: Reya-Labs/reya-python-sdk

Length of output: 51


🏁 Script executed:

# List all Python files in sdk/ to understand core SDK structure
find sdk -name "*.py" -type f | head -30

Repository: Reya-Labs/reya-python-sdk

Length of output: 1123


🏁 Script executed:

# Read sdk/__init__.py to see what's exported
cat -n sdk/__init__.py

Repository: Reya-Labs/reya-python-sdk

Length of output: 51


🏁 Script executed:

# Search for any imports from telegram in the entire SDK (not telegram_bot package)
rg "from telegram|import telegram" . --type py -g "!telegram_bot/" 2>/dev/null

Repository: Reya-Labs/reya-python-sdk

Length of output: 51


🏁 Script executed:

# Check if telegram_bot is referenced in any SDK code, examples, or documentation
rg "telegram_bot" sdk/ examples/ --type py 2>/dev/null | head -20

Repository: Reya-Labs/reya-python-sdk

Length of output: 51


🏁 Script executed:

# Check examples directory to understand how telegram_bot is meant to be used
find examples -type f -name "*.py" 2>/dev/null | xargs ls -la

Repository: Reya-Labs/reya-python-sdk

Length of output: 2660


🏁 Script executed:

# Look at the telegram_bot/__init__.py to see what's exported
cat -n telegram_bot/__init__.py

Repository: Reya-Labs/reya-python-sdk

Length of output: 121


🏁 Script executed:

# Check the README to see if telegram bot is documented as a core feature or optional feature
head -50 README.md

Repository: Reya-Labs/reya-python-sdk

Length of output: 2224


Move python-telegram-bot to an optional extra.

The python-telegram-bot>=21.0,<22.0 dependency is currently in the core SDK dependencies but is only used by the self-contained telegram_bot module. The core SDK and its examples have zero dependency on this library. Including it in base installations forces every consumer to download and maintain an unnecessary dependency, even those who never use the bot. Move python-telegram-bot behind an optional extra (e.g., telegram) so only users who explicitly use the bot feature install it.

Proposed packaging change
 dependencies = [
     "web3>=7.12.1,<8.0.0",
     "eth-account>=0.13.7,<1.0.0",
     "eth-abi>=5.2.0,<6.0.0",
     "typing-extensions>=4.14.1,<5.0.0",
     "python-dotenv>=1.1.1,<2.0.0",
     "requests>=2.32.4,<3.0.0",
     "httpx>=0.28.1,<1.0.0",
     "websocket-client>=1.8.0,<2.0.0",
     "tomli>=2.0.1,<3.0.0",
     "types-requests>=2.31.0,<2.32.0",
     "python-dateutil>=2.8.0,<3.0.0",
     "aiohttp-retry>=2.8.3,<3.0.0",
-    "python-telegram-bot>=21.0,<22.0"
 ]

 [project.optional-dependencies]
 dev = [
     ...
 ]
+telegram = [
+    "python-telegram-bot>=21.0,<22.0",
+]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"aiohttp-retry>=2.8.3,<3.0.0",
"python-telegram-bot>=21.0,<22.0"
"aiohttp-retry>=2.8.3,<3.0.0",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pyproject.toml` around lines 38 - 39, Remove the
"python-telegram-bot>=21.0,<22.0" entry from the core dependencies list and add
it as an optional extra so only consumers who opt into the bot feature install
it; specifically, locate the dependency string "python-telegram-bot>=21.0,<22.0"
in pyproject.toml and delete it from the main dependencies, then add it under
the PEP-621 optional dependencies section (e.g., add an entry under
[project.optional-dependencies] with a key like "telegram" whose value is
["python-telegram-bot>=21.0,<22.0"]); ensure the extras key name ("telegram")
matches any docs or usage in the repo that reference installing the telegram
feature.

Comment on lines +10 to +12
from telegram import Update
from telegram.constants import ParseMode
from telegram.ext import Application, CommandHandler, ContextTypes
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

git ls-files | grep -E "telegram_bot|bot\.py"

Repository: Reya-Labs/reya-python-sdk

Length of output: 185


🏁 Script executed:

cat -n telegram_bot/bot.py

Repository: Reya-Labs/reya-python-sdk

Length of output: 18797


🌐 Web query:

telegram-bot-api python BadRequest exception parse error

💡 Result:

In Telegram’s Bot API, a Python BadRequest with a parse error almost always means Telegram rejected the formatting or JSON you sent, not that your HTTP request failed.

1) Bad Request: can't parse entities ... (most common)

You sent parse_mode="HTML" or parse_mode="MarkdownV2" (or Markdown) and the text contains invalid / unescaped markup, e.g.:

  • HTML: unsupported tags (<br>), malformed tags, missing closing tags, raw < / & that should be escaped. Aiogram shows this exact failure pattern with “Unsupported start tag … at byte offset …”. [2]
  • MarkdownV2: reserved characters not escaped (e.g. (, ), _, *, [, ], etc.), or unbalanced formatting markers—this commonly triggers “Can’t find end of the entity …”. [4]

Fix options (pick one):

  • Disable parsing (send plain text): don’t set parse_mode.
  • Escape your text:
    • python-telegram-bot: use telegram.utils.helpers.escape_markdown(...) before sending with Markdown/MarkdownV2. [3]
    • For HTML parse mode: escape user-generated text with html.escape(...) and only add a small, valid subset of tags yourself (Telegram only supports a limited HTML subset). [1]
  • Avoid parse_mode entirely by sending explicit entities (message entities) instead of markup. (Supported by many libraries and the Bot API conceptually.) [1]

2) Bad Request: can't parse reply keyboard markup JSON object / other “can’t parse … JSON”

Your reply_markup / payload is not valid JSON (common when sending dicts via querystring, wrong nesting, or not JSON-serializing).

Fix:

  • Send as POST JSON (not URL params) and ensure reply_markup is properly serialized (your library usually does this if you pass the right Python objects).

3) Use the offset to find the culprit

The error’s “byte offset …” points to where Telegram’s parser got confused—inspect that character position in the final string you sent (after any templating/formatting).

If you paste the exact exception text + the message text (and which library: python-telegram-bot, aiogram, pyTelegramBotAPI, or raw requests), I can point to the specific character/tag and show the minimal escaping needed.


🌐 Web query:

python-telegram-bot BadRequest exception telegram.error

💡 Result:

In python-telegram-bot (PTB), telegram.error.BadRequest is the library’s wrapper for Telegram Bot API “400 Bad Request” responses—i.e., Telegram accepted the HTTP request but rejected the parameters/state as invalid. (docs.python-telegram-bot.org)

What to do first

  1. Print/log the exact error text (it’s in the exception message). PTB’s BadRequest message usually contains the Telegram reason like:
    • Message is not modified
    • Chat not found
    • Message to edit not found
  2. Handle it either locally (try/except) or via PTB’s global error handler. (github-wiki-see.page)

Common BadRequest causes (and fixes)

1) Message is not modified

Happens when you call edit_message_text / edit_message_reply_markup but the new text/markup is identical to the current one. Fix: compare before editing, or ignore that specific error. (stackoverflow.com)

2) Chat not found

Usually means the chat_id is wrong, you’re trying to message a user who hasn’t started the bot, the bot isn’t in the group/channel, or you’re using the wrong ID type (e.g., missing -100... for many channels/supergroups). (github.com)

3) Message to edit not found

You’re trying to edit a message that doesn’t exist (wrong message_id), is too old/invalid to edit, or belongs to a different chat. (stackoverflow.com)

Minimal handling pattern

from telegram.error import BadRequest, TelegramError

try:
    await update.callback_query.edit_message_text("new text")
except BadRequest as e:
    if "Message is not modified" in str(e):
        return  # safe to ignore
    raise
except TelegramError:
    raise

(Using a global error handler is also supported/recommended for centralized logging/handling.) (github-wiki-see.page)

If you paste the exact exception text (full line after BadRequest:) and the API call you’re making (e.g., send_message, edit_message_text, etc.), I can map it to the specific fix.

Citations:


🌐 Web query:

python-telegram-bot reply_text exceptions TelegramError BadRequest NetworkError

💡 Result:

In python-telegram-bot (PTB), await message.reply_text(...) ultimately makes a Bot API request (i.e., “sendMessage”), and failures are raised as telegram.error.TelegramError (base class) or one of its subclasses. PTB’s exception types live in telegram.error. [1]

The 3 you mentioned (what they mean)

  • TelegramError: Catch-all base class for PTB/Telegram-related errors. [1]
  • BadRequest (subclass of NetworkError): Telegram Bot API returned HTTP 400 because your request parameters/content are invalid (e.g., empty/too-long text, invalid parse_mode/entities, bad markup, wrong IDs, “message thread not found”, etc.). [1][2]
  • NetworkError: Network/communication layer problems when talking to Telegram (connectivity, protocol issues, transient failures, etc.). BadRequest is also under this umbrella in PTB’s hierarchy. [1]

Typical handling pattern

from telegram.error import BadRequest, NetworkError, TelegramError

try:
    await update.message.reply_text(text, parse_mode="HTML")
except BadRequest as e:
    # Fix the request: bad HTML/entities, empty text, invalid reply_markup, wrong thread/chat, etc.
    logger.warning("BadRequest: %s", e)
except NetworkError as e:
    # Transient: you may retry/backoff
    logger.warning("NetworkError: %s", e)
except TelegramError as e:
    # Anything else PTB raises
    logger.exception("TelegramError: %s", e)

If you share the exact exception message (e.g., "Bad Request: can't parse entities", "message text is empty", "message thread not found"), I can map it to the most likely concrete cause and fix.

Sources: PTB telegram.error docs (exception definitions & hierarchy). [1] PTB error-handling notes/examples of common BadRequest scenarios. [2]

[1] (docs.python-telegram-bot.org)
[2] (deepwiki.com)

Citations:


Narrow exception handling to parse errors only.

The _reply helper currently catches all exceptions and silently falls back to plain text, which masks unrelated Telegram API errors and network failures. This should catch only BadRequest exceptions with parse-related messages, allowing other errors to propagate.

Suggested exception narrowing
 from telegram import Update
+from telegram.error import BadRequest
 from telegram.constants import ParseMode
 from telegram.ext import Application, CommandHandler, ContextTypes
 ...
 async def _reply(update: Update, text: str) -> None:
     """Send a Markdown reply, falling back to plain text on parse errors."""
     try:
         await update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN)
-    except Exception:
+    except BadRequest as exc:
+        if "parse" not in str(exc).lower():
+            raise
         await update.message.reply_text(text)

Also applies to: 64-69

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@telegram_bot/bot.py` around lines 10 - 12, The _reply helper (and the other
try/except at lines ~64-69) currently catches all exceptions; change them to
catch only telegram.error.BadRequest and then inspect the exception message for
parse-related indicators (e.g., "can't parse entities", "Can't parse", or
similar parse/markdown/html errors) before falling back to plain text; for any
other BadRequest messages or any other exception types re-raise the exception so
network/Telegram API errors are not swallowed. Ensure you import BadRequest from
telegram.error and update both exception blocks (the _reply function and the
second handler around lines 64-69) to follow this narrow-catch-and-inspect
pattern.

Comment on lines +39 to +45
*Trading — Perp / Spot IOC (fill-or-kill)*
`/buy <SYMBOL> <QTY> <PRICE>` — Limit buy (IOC)
`/sell <SYMBOL> <QTY> <PRICE>` — Limit sell (IOC)

*Trading — Perp GTC (resting limit order)*
`/buygtc <SYMBOL> <QTY> <PRICE>` — Limit buy (GTC)
`/sellgtc <SYMBOL> <QTY> <PRICE>` — Limit sell (GTC)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Document IOC correctly; the current help text describes FOK semantics.

IOC orders can partially fill and cancel the remainder. Calling them “fill-or-kill” tells users the order is all-or-nothing, which is a materially different trading behavior.

Suggested wording fix
-*Trading — Perp / Spot IOC (fill-or-kill)*
+*Trading — Perp / Spot IOC (immediate-or-cancel)*
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
*TradingPerp / Spot IOC (fill-or-kill)*
`/buy <SYMBOL> <QTY> <PRICE>`Limit buy (IOC)
`/sell <SYMBOL> <QTY> <PRICE>`Limit sell (IOC)
*TradingPerp GTC (resting limit order)*
`/buygtc <SYMBOL> <QTY> <PRICE>`Limit buy (GTC)
`/sellgtc <SYMBOL> <QTY> <PRICE>`Limit sell (GTC)
*TradingPerp / Spot IOC (immediate-or-cancel)*
`/buy <SYMBOL> <QTY> <PRICE>`Limit buy (IOC)
`/sell <SYMBOL> <QTY> <PRICE>`Limit sell (IOC)
*TradingPerp GTC (resting limit order)*
`/buygtc <SYMBOL> <QTY> <PRICE>`Limit buy (GTC)
`/sellgtc <SYMBOL> <QTY> <PRICE>`Limit sell (GTC)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@telegram_bot/bot.py` around lines 39 - 45, The help text mislabels IOC orders
as “fill-or-kill” (FOK); update the trading help block that lists the /buy,
/sell, /buygtc and /sellgtc command descriptions to correctly describe IOC as
"Immediate-or-Cancel (IOC) — executes immediately and cancels any unfilled
portion (partial fills allowed)" and keep GTC described as resting limit orders;
locate the help text string in bot.py (the block that contains "*Trading — Perp
/ Spot IOC (fill-or-kill)*" and replace the wording accordingly so the /buy and
/sell entries reference IOC semantics rather than FOK.

Comment on lines +211 to +220
def _parse_order_args(args: list[str]) -> tuple[str, str, str]:
"""Parse and validate <SYMBOL> <QTY> <PRICE> args. Returns (symbol, qty, price)."""
if len(args) < 3:
raise ValueError("Usage: `<SYMBOL> <QTY> <PRICE>`")
symbol = args[0].upper()
qty = args[1]
price = args[2]
float(qty) # validate numeric
float(price) # validate numeric
return symbol, qty, price
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reject non-positive and non-finite numeric inputs before order submission.

float(...) only checks that the string parses. Right now 0, negative values, nan, and inf all pass validation and can reach the trading API for both order placement and trigger creation.

Suggested validation hardening
+import math
 ...
+def _parse_positive_number(name: str, raw: str) -> str:
+    value = float(raw)
+    if not math.isfinite(value) or value <= 0:
+        raise ValueError(f"{name} must be a positive finite number.")
+    return raw
+
 ...
 def _parse_order_args(args: list[str]) -> tuple[str, str, str]:
     """Parse and validate <SYMBOL> <QTY> <PRICE> args. Returns (symbol, qty, price)."""
     if len(args) < 3:
         raise ValueError("Usage: `<SYMBOL> <QTY> <PRICE>`")
     symbol = args[0].upper()
-    qty = args[1]
-    price = args[2]
-    float(qty)   # validate numeric
-    float(price)  # validate numeric
+    qty = _parse_positive_number("QTY", args[1])
+    price = _parse_positive_number("PRICE", args[2])
     return symbol, qty, price
 ...
 def _parse_trigger_args(args: list[str]) -> tuple[str, bool, str]:
     """Parse <SYMBOL> <buy|sell> <TRIGGER_PRICE>. Returns (symbol, is_buy, trigger_px)."""
     if len(args) < 3:
         raise ValueError("Usage: `<SYMBOL> <buy|sell> <TRIGGER_PRICE>`")
     symbol = args[0].upper()
     side = args[1].lower()
     if side not in ("buy", "sell"):
         raise ValueError("Side must be `buy` or `sell`.")
     is_buy = side == "buy"
-    trigger_px = args[2]
-    float(trigger_px)  # validate numeric
+    trigger_px = _parse_positive_number("TRIGGER_PRICE", args[2])
     return symbol, is_buy, trigger_px

Also applies to: 301-312

🧰 Tools
🪛 Ruff (0.15.4)

[warning] 214-214: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@telegram_bot/bot.py` around lines 211 - 220, The _parse_order_args function
currently only attempts float(qty)/float(price) which allows zero, negatives,
NaN, and infinite values; update _parse_order_args to convert qty and price to
floats, then reject values that are <= 0 or not finite (use math.isfinite),
raising ValueError with the same usage/help text if validation fails; keep
returning symbol, qty, price (either as validated floats or original strings per
the surrounding API expectations) and apply the same finite/positive checks to
the other order-parsing/validation block mentioned in the review.

Comment on lines +68 to +80
for pos in positions:
symbol = getattr(pos, "symbol", "N/A")
side = "Long" if getattr(pos, "is_long", True) else "Short"
size_raw = getattr(pos, "size", None)
size_str = f"{float(size_raw) / 1e18:,.6f}" if size_raw else "N/A"
entry_raw = getattr(pos, "avg_entry_price", None)
entry_str = _oracle_price_usd(entry_raw) if entry_raw else "N/A"
pnl_raw = getattr(pos, "unrealized_pnl", None)
pnl_str = f"{float(pnl_raw) / 1e18:,.4f}" if pnl_raw else "N/A"
lines.append(
f"`{symbol}` — {side} {size_str}\n"
f" Entry: {entry_str} | uPnL: {pnl_str} rUSD"
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

fmt_positions is reading fields the SDK model does not expose.

sdk/open_api/models/position.py:27-41 has side, qty, and avg_entry_price; this formatter looks for is_long, size, and unrealized_pnl. In practice that will default real positions to Long, print N/A for size, and never show actual PnL.

Suggested field alignment
     for pos in positions:
         symbol = getattr(pos, "symbol", "N/A")
-        side = "Long" if getattr(pos, "is_long", True) else "Short"
-        size_raw = getattr(pos, "size", None)
+        side = getattr(pos, "side", "N/A")
+        size_raw = getattr(pos, "qty", None)
         size_str = f"{float(size_raw) / 1e18:,.6f}" if size_raw else "N/A"
         entry_raw = getattr(pos, "avg_entry_price", None)
         entry_str = _oracle_price_usd(entry_raw) if entry_raw else "N/A"
-        pnl_raw = getattr(pos, "unrealized_pnl", None)
-        pnl_str = f"{float(pnl_raw) / 1e18:,.4f}" if pnl_raw else "N/A"
         lines.append(
             f"`{symbol}` — {side} {size_str}\n"
-            f"  Entry: {entry_str}  |  uPnL: {pnl_str} rUSD"
+            f"  Entry: {entry_str}"
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@telegram_bot/formatters.py` around lines 68 - 80, fmt_positions is reading
non-existent SDK fields (is_long, size, unrealized_pnl); update it to use the
SDK model's actual attributes: use pos.side (map to "Long"/"Short" as
appropriate) instead of is_long, use pos.qty for size display (format pos.qty /
1e18) instead of size, and use pos.avg_entry_price for entry price display;
remove or fallback to "N/A" for unrealized_pnl (or use any available pnl field
from the SDK if present) so the formatter shows real data from the SDK model
rather than default values.

Comment on lines +89 to +103
for o in orders:
order_id = getattr(o, "order_id", "N/A")
symbol = getattr(o, "symbol", "N/A")
side = "Buy" if getattr(o, "is_buy", True) else "Sell"
qty_raw = getattr(o, "qty", None)
qty_str = f"{float(qty_raw) / 1e18:,.6f}" if qty_raw else "N/A"
px_raw = getattr(o, "limit_px", None)
px_str = _oracle_price_usd(px_raw) if px_raw else "N/A"
order_type = getattr(o, "order_type", "")
tif = getattr(o, "time_in_force", "")
status = getattr(o, "status", "")
lines.append(
f"ID `{order_id}` — {side} {qty_str} `{symbol}` @ {px_str}\n"
f" Type: {order_type} {tif} | Status: {status}"
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Open-order side will display incorrectly for SDK orders.

sdk/open_api/models/order.py:30-50 exposes side, not is_buy. With the current getattr(o, "is_buy", True) fallback, sell orders from the SDK render as buys.

Suggested field fix
-        side = "Buy" if getattr(o, "is_buy", True) else "Sell"
+        side = getattr(o, "side", "N/A")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@telegram_bot/formatters.py` around lines 89 - 103, The loop that formats
orders uses getattr(o, "is_buy", True) causing SDK Order objects (which expose
side, not is_buy) to be mis-labeled; update the logic in the orders formatting
block to prefer getattr(o, "side", None) and map its string (e.g., "buy"/"Buy"
or "sell"/"Sell") to the display "Buy"/"Sell", falling back to using getattr(o,
"is_buy", None) (mapping True→"Buy", False→"Sell") only if side is missing, and
use a safe default like "N/A" if neither is present; adjust the reference in the
lines.append construction so the variable used for display is the normalized
side string instead of the current side expression.

Comment on lines +147 to +153
for s in summaries:
symbol = getattr(s, "symbol", "N/A")
last_raw = getattr(s, "last_price", None)
last_str = _oracle_price_usd(last_raw) if last_raw else "N/A"
volume_raw = getattr(s, "volume_24h", None)
volume_str = f"{float(volume_raw) / 1e18:,.2f}" if volume_raw else "N/A"
lines.append(f"`{symbol}` — Last: {last_str} Vol 24h: {volume_str}")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

/markets will show N/A because these attributes do not exist on MarketSummary.

sdk/open_api/models/market_summary.py:26-44 exposes throttled_oracle_price and volume24h; last_price and volume_24h are not part of the model.

Suggested field fix
-        last_raw = getattr(s, "last_price", None)
+        last_raw = getattr(s, "throttled_oracle_price", None)
         last_str = _oracle_price_usd(last_raw) if last_raw else "N/A"
-        volume_raw = getattr(s, "volume_24h", None)
+        volume_raw = getattr(s, "volume24h", None)
         volume_str = f"{float(volume_raw) / 1e18:,.2f}" if volume_raw else "N/A"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for s in summaries:
symbol = getattr(s, "symbol", "N/A")
last_raw = getattr(s, "last_price", None)
last_str = _oracle_price_usd(last_raw) if last_raw else "N/A"
volume_raw = getattr(s, "volume_24h", None)
volume_str = f"{float(volume_raw) / 1e18:,.2f}" if volume_raw else "N/A"
lines.append(f"`{symbol}` — Last: {last_str} Vol 24h: {volume_str}")
for s in summaries:
symbol = getattr(s, "symbol", "N/A")
last_raw = getattr(s, "throttled_oracle_price", None)
last_str = _oracle_price_usd(last_raw) if last_raw else "N/A"
volume_raw = getattr(s, "volume24h", None)
volume_str = f"{float(volume_raw) / 1e18:,.2f}" if volume_raw else "N/A"
lines.append(f"`{symbol}` — Last: {last_str} Vol 24h: {volume_str}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@telegram_bot/formatters.py` around lines 147 - 153, The loop in summaries
uses non-existent attributes last_price and volume_24h on MarketSummary; replace
them with the actual model fields throttled_oracle_price and volume24h (e.g.,
get throttled_oracle_price into last_raw and pass it to _oracle_price_usd, and
get volume24h into volume_raw and format as before), using getattr(s,
"throttled_oracle_price", None) and getattr(s, "volume24h", None) so the
`/markets` output shows real values.

Comment on lines +78 to +81
if allowed_user_ids:
logger.info("Access restricted to user IDs: %s", allowed_user_ids)
else:
logger.warning("ALLOWED_USER_IDS not set — all Telegram users can interact with the bot")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fail closed when no Telegram allow-list is configured.

With the current flow, omitting ALLOWED_USER_IDS exposes a hot trading bot to any Telegram user who can discover it. For a bot holding signing credentials, that is an authentication bypass and should not be the default. Require an allow-list unless the operator explicitly opts into public mode.

Suggested fail-closed guard
     logger.info("Chain ID: %d (%s)", config.chain_id, "mainnet" if config.is_mainnet else "testnet")

-    if allowed_user_ids:
+    allow_public_bot = os.environ.get("ALLOW_PUBLIC_BOT", "").lower() == "true"
+    if allowed_user_ids:
         logger.info("Access restricted to user IDs: %s", allowed_user_ids)
+    elif allow_public_bot:
+        logger.warning("Running in public mode — all Telegram users can interact with the bot")
     else:
-        logger.warning("ALLOWED_USER_IDS not set — all Telegram users can interact with the bot")
+        logger.error("ALLOWED_USER_IDS is required unless ALLOW_PUBLIC_BOT=true is set explicitly.")
+        sys.exit(1)
 ...
-    if allowed_user_ids:
+    if allowed_user_ids:
         _apply_access_control(app, allowed_user_ids)

Also applies to: 95-96

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@telegram_bot/main.py` around lines 78 - 81, The code currently logs a warning
when ALLOWED_USER_IDS is unset, which leaves the bot open; change the behavior
to fail-closed by requiring ALLOWED_USER_IDS unless an explicit opt-in flag
(e.g., TELEGRAM_PUBLIC_MODE=true) is set. Update the check around
allowed_user_ids in main.py (the variable allowed_user_ids and the logger calls)
to: if allowed_user_ids is empty and TELEGRAM_PUBLIC_MODE is not truthy, log an
error and exit (or raise SystemExit); if TELEGRAM_PUBLIC_MODE is set, keep the
existing info/warning behavior. Apply the same change to the other occurrence
referenced (lines 95-96) so both checks enforce the fail-closed policy.

Comment on lines +126 to +139
from telegram import Update
from telegram.ext import TypeHandler

async def _check_user(update: Update, context) -> None:
if not update.effective_user:
return
uid = update.effective_user.id
if uid not in allowed_user_ids:
logger.warning("Rejected update from unauthorised user %d", uid)
if update.message:
await update.message.reply_text("You are not authorised to use this bot.")
raise Exception("Unauthorised")

app.add_handler(TypeHandler(Update, _check_user), group=-1)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🌐 Web query:

ApplicationHandlerStop python-telegram-bot telegram.ext

💡 Result:

telegram.ext.ApplicationHandlerStop is an exception you can raise inside a handler (or error handler) to stop python-telegram-bot’s Application from running any further handlers for the same update, even across different handler groups. It’s the “hard stop” mechanism for handler dispatching. [1]

Typical use

from telegram.ext import ApplicationHandlerStop

async def my_handler(update, context):
    # ... do something ...
    raise ApplicationHandlerStop()  # stop processing this update in other handlers

[1]

With ConversationHandler

Instead of returning the next state, pass it via the optional state argument:

from telegram.ext import ApplicationHandlerStop

async def conversation_callback(update, context):
    # ...
    raise ApplicationHandlerStop(next_state)

[1]

Important caveat

It doesn’t work if the handler/error-handler is executed in a non-blocking way. [1]

(Older v13 docs called the analogous concept DispatcherHandlerStop; in v20+ it’s ApplicationHandlerStop.) [1]

Sources: python-telegram-bot docs for ApplicationHandlerStop. [1]


🏁 Script executed:

# Search for ApplicationHandlerStop in the codebase if it exists
rg "ApplicationHandlerStop" .

Repository: Reya-Labs/reya-python-sdk

Length of output: 51


🌐 Web query:

python-telegram-bot exception handling handlers error pipeline

💡 Result:

In python-telegram-bot (PTB) v20+, the unhandled-exception pipeline works like this:

  • If a handler callback (or a job callback) raises an exception and you don’t catch it yourself, PTB routes it to Application.process_error(). (docs.python-telegram-bot.org)
  • process_error() then forwards the exception to all error handlers you registered via Application.add_error_handler(...). (docs.python-telegram-bot.org)
  • The error-handler callback receives the originating update (or None) and a context whose context.error is the exception. (github-wiki-see.page)
  • If one error handler raises ApplicationHandlerStop, PTB stops calling further error handlers and (when the error came from update processing) stops processing that update in any remaining handler groups. (docs.python-telegram-bot.org)
  • If you register no error handler, PTB will log unhandled exceptions. (github-wiki-see.page)

Minimal pattern (PTB v20+)

from telegram.ext import Application, ContextTypes

async def on_error(update, context: ContextTypes.DEFAULT_TYPE) -> None:
    err = context.error  # the exception
    # log it, notify admins, etc.

app = Application.builder().token("TOKEN").build()
app.add_error_handler(on_error)  # receives every uncaught error in the bot

add_error_handler(..., block=True) controls whether PTB awaits each error handler before running the next one. (docs.python-telegram-bot.org)

Practical guidance

  • Use try/except inside specific handlers for “expected” failures (API 403, timeouts, parse errors), and reserve the global error handler as a last resort for truly unexpected exceptions. (github-wiki-see.page)

Citations:


🏁 Script executed:

# Check the full main.py file around lines 126-139
head -n 160 telegram_bot/main.py | tail -n 50

Repository: Reya-Labs/reya-python-sdk

Length of output: 1607


🏁 Script executed:

# Search for error handlers in the codebase
rg "add_error_handler|process_error" .

Repository: Reya-Labs/reya-python-sdk

Length of output: 51


🏁 Script executed:

# Check for any try/except blocks around the handler logic
rg "try:|except" telegram_bot/main.py -A 2 -B 2

Repository: Reya-Labs/reya-python-sdk

Length of output: 1345


Use ApplicationHandlerStop for blocked users instead of raising Exception.

Authorization rejection is normal control flow, not an error. Raising a generic exception routes every rejected update through PTB's error pipeline and creates noisy stack traces in logs that obscure actual problems.

Suggested PTB-native short-circuit
-    from telegram.ext import TypeHandler
+    from telegram.ext import ApplicationHandlerStop, TypeHandler
 ...
     async def _check_user(update: Update, context) -> None:
         if not update.effective_user:
             return
         uid = update.effective_user.id
         if uid not in allowed_user_ids:
             logger.warning("Rejected update from unauthorised user %d", uid)
             if update.message:
                 await update.message.reply_text("You are not authorised to use this bot.")
-            raise Exception("Unauthorised")
+            raise ApplicationHandlerStop
🧰 Tools
🪛 Ruff (0.15.4)

[warning] 129-129: Unused function argument: context

(ARG001)


[warning] 137-137: Create your own exception

(TRY002)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@telegram_bot/main.py` around lines 126 - 139, The _check_user handler
currently raises a generic Exception to stop processing unauthorized updates;
change this to raise telegram.ext.ApplicationHandlerStop (import
ApplicationHandlerStop) so PTB treats it as normal short-circuit control flow
rather than an error, and keep the existing reply_text call before raising
ApplicationHandlerStop in _check_user (or simply return after sending the reply
if you prefer to avoid raising); update the import list to include
ApplicationHandlerStop and remove the generic Exception usage in the
TypeHandler(Update, _check_user) flow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants