<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alexey Polyakov</title>
    <description>The latest articles on DEV Community by Alexey Polyakov (@alexey_polyakov_cfe2095e3).</description>
    <link>https://dev.to/alexey_polyakov_cfe2095e3</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3977419%2F4ba77741-d2f9-4ae7-be8e-4683889ce5fc.jpg</url>
      <title>DEV Community: Alexey Polyakov</title>
      <link>https://dev.to/alexey_polyakov_cfe2095e3</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alexey_polyakov_cfe2095e3"/>
    <language>en</language>
    <item>
      <title>I Built an AI-Native Trading Engine in Python. 5 Months Later, Here's What Changed</title>
      <dc:creator>Alexey Polyakov</dc:creator>
      <pubDate>Wed, 17 Jun 2026 10:44:34 +0000</pubDate>
      <link>https://dev.to/alexey_polyakov_cfe2095e3/i-built-an-ai-native-trading-engine-in-python-5-months-later-heres-what-changed-317</link>
      <guid>https://dev.to/alexey_polyakov_cfe2095e3/i-built-an-ai-native-trading-engine-in-python-5-months-later-heres-what-changed-317</guid>
      <description>&lt;h1&gt;
  
  
  I Built an AI-Native Trading Engine in Python. 5 Months Later, Here's What Changed
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;9 strategies → 12. ML scoring, backtesting, partial take-profit, Telegram bot that survived a 3-echelon audit. Open source, MIT, trading real money.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why I Built This (And Kept Building)
&lt;/h2&gt;

&lt;p&gt;Trading bots come in two flavors: black-box SaaS at $50/month, or GitHub scripts that crash at 3 AM. I wanted neither.&lt;/p&gt;

&lt;p&gt;Five months ago I shipped v1 of &lt;strong&gt;bybit-ws&lt;/strong&gt; — an AI-native trading engine for Bybit futures. The core loop was simple: scan Bollinger Bands daily, score every signal across 8 metrics, enter when the score passes 5.5/10. It worked. It made money. And then I kept building.&lt;/p&gt;

&lt;p&gt;This is the v2 story: machine learning on real trade data, a backtesting engine that runs against historical klines, partial take-profit logic, and a Telegram bot that survived 14 production bugs found by three AI agents in parallel.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture: What 5 Months of Iteration Looks Like
&lt;/h2&gt;

&lt;p&gt;v1 was clean. v2 is battle-tested.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    ┌──────────────────────────┐
                    │      Hermes Agent          │
                    │  Voice/chat orchestrator   │
                    └───────────┬────────────────┘
                                │ MCP / REST (port 8766)
                    ┌───────────▼────────────────┐
                    │     bybit-ws (systemd)      │
                    │                             │
                    │  main.py — 30s light cycle  │
                    │         — 120s heavy cycle  │
                    │         — 480s x10 cycle    │
                    │                             │
                    │  ┌─ auto_sl.py              │
                    │  ├─ trailing_sl.py           │
                    │  ├─ trailing_sl_x10.py   ★   │
                    │  ├─ partial_tp.py        ★   │
                    │  ├─ funding_rotation.py  ★   │
                    │  ├─ ml_scorer.py         ★   │
                    │  ├─ backtest.py          ★   │
                    │  ├─ gridsignal_scanner.py    │
                    │  ├─ pump_detect.py           │
                    │  ├─ rpc.py (JSON-RPC)        │
                    │  └─ state_db.py (SQLite)     │
                    └───────────┬────────────────┘
                                │ WebSocket
                    ┌───────────▼────────────────┐
                    │       Bybit API v5          │
                    └────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;★ = new in v2&lt;/p&gt;

&lt;p&gt;Three cycles instead of two. The new &lt;strong&gt;x10 cycle&lt;/strong&gt; (every 8 minutes) handles trailing stops for high-leverage positions without touching the 3x positions. The heavy cycle (every 2 minutes, down from 7) now includes partial take-profit and funding rotation checks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State is SQLite, period.&lt;/strong&gt; v1 had a mix of in-memory dicts and JSON snapshots. v2 uses SQLite with WAL mode as the single source of truth. 8 tables, atomic UPDATEs with WHERE clauses, zero race conditions. JSON snapshots are backup-only.&lt;/p&gt;




&lt;h2&gt;
  
  
  12 Strategies (Up from 9)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Lev&lt;/th&gt;
&lt;th&gt;Timeframe&lt;/th&gt;
&lt;th&gt;What's New&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bollinger Grid LONG&lt;/td&gt;
&lt;td&gt;3x&lt;/td&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bollinger Grid SHORT&lt;/td&gt;
&lt;td&gt;3x&lt;/td&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Junk Short&lt;/td&gt;
&lt;td&gt;3x&lt;/td&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SL Re-entry&lt;/td&gt;
&lt;td&gt;3x&lt;/td&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DCA Ladder&lt;/td&gt;
&lt;td&gt;3x&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BB Scalping&lt;/td&gt;
&lt;td&gt;10x&lt;/td&gt;
&lt;td&gt;M5&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mean Reversion&lt;/td&gt;
&lt;td&gt;10x&lt;/td&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Funding Momentum&lt;/td&gt;
&lt;td&gt;10x&lt;/td&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ATR Risk Sizing&lt;/td&gt;
&lt;td&gt;layer&lt;/td&gt;
&lt;td&gt;15m&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Partial TP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;★&lt;/td&gt;
&lt;td&gt;dynamic&lt;/td&gt;
&lt;td&gt;20→50% scale-out, no numpy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Trailing SL x10&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;★&lt;/td&gt;
&lt;td&gt;x10 only&lt;/td&gt;
&lt;td&gt;Tight trailing on high-lev positions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Funding Rotation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;★&lt;/td&gt;
&lt;td&gt;auto&lt;/td&gt;
&lt;td&gt;Closes positions before negative funding hits&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The golden rule still applies: every entry passes through &lt;strong&gt;scoring across 8+ metrics&lt;/strong&gt; with a 5.5/10 threshold. But now there's a new layer on top.&lt;/p&gt;




&lt;h2&gt;
  
  
  ML Scoring: When Heuristics Aren't Enough
&lt;/h2&gt;

&lt;p&gt;After 5 months, the system had logged &lt;strong&gt;282 real signals&lt;/strong&gt; with outcomes — entry price, exit price, PnL, whether it hit take-profit or stop-loss. That's a dataset.&lt;/p&gt;

&lt;p&gt;I trained a &lt;strong&gt;RandomForest classifier&lt;/strong&gt; on it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;features&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;price_vs_lower&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;price_vs_upper&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;volume_24h&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;funding_rate&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rsi_14&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bb_squeeze&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;consecutive_down_days&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;target&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;is_profitable&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;  &lt;span class="c1"&gt;# 1 if PnL &amp;gt; 0, else 0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;F1 score: 0.69&lt;/strong&gt; on a 262-signal test set&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top features:&lt;/strong&gt; score (0.31 weight), price_vs_lower (0.22), RSI (0.15)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Combined scoring:&lt;/strong&gt; 70% ML + 30% heuristic. If they disagree, ML wins.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model runs every heavy cycle, re-scores open positions, and can veto new entries. It's not a black box — feature importance is logged, so I can see &lt;em&gt;why&lt;/em&gt; it made a decision.&lt;/p&gt;

&lt;p&gt;Is F1=0.69 "AI that prints money"? No. It's a filter that catches bad entries the heuristic would miss. In backtesting, the ML layer improved average PnL per trade by 18% just by rejecting the bottom quartile of signals.&lt;/p&gt;




&lt;h2&gt;
  
  
  Backtesting: Walk-Forward on Real Klines
&lt;/h2&gt;

&lt;p&gt;You can't trust a strategy you haven't backtested. I built a walk-forward engine that pulls historical klines from Bybit's REST API and replays them day by day:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Each day: scan → score → simulate entry → track PnL
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;trading_days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;klines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_klines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;signals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;klines&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strategy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bollinger_grid&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;trade&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;simulate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;klines&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trade&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tested on BTC, ETH, SUI, and ADA with Daily signals:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symbol&lt;/th&gt;
&lt;th&gt;Win Rate&lt;/th&gt;
&lt;th&gt;Avg PnL&lt;/th&gt;
&lt;th&gt;Best Trade&lt;/th&gt;
&lt;th&gt;Worst Trade&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SUIUSDT&lt;/td&gt;
&lt;td&gt;42.9%&lt;/td&gt;
&lt;td&gt;+6.56%&lt;/td&gt;
&lt;td&gt;+27.3%&lt;/td&gt;
&lt;td&gt;−12.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ADAUSDT&lt;/td&gt;
&lt;td&gt;38.5%&lt;/td&gt;
&lt;td&gt;+4.82%&lt;/td&gt;
&lt;td&gt;+19.4%&lt;/td&gt;
&lt;td&gt;−11.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BTCUSDT&lt;/td&gt;
&lt;td&gt;35.7%&lt;/td&gt;
&lt;td&gt;+3.11%&lt;/td&gt;
&lt;td&gt;+14.2%&lt;/td&gt;
&lt;td&gt;−9.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ETHUSDT&lt;/td&gt;
&lt;td&gt;33.3%&lt;/td&gt;
&lt;td&gt;+1.95%&lt;/td&gt;
&lt;td&gt;+11.8%&lt;/td&gt;
&lt;td&gt;−10.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Not every strategy is a winner. The backtest exposed that ETH is borderline — win rate barely above random, average PnL close to fees. That's valuable information. I'd rather know from backtesting than from a blown account.&lt;/p&gt;




&lt;h2&gt;
  
  
  Risk Management: The Boring Stuff That Saves Accounts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Partial Take-Profit
&lt;/h3&gt;

&lt;p&gt;Most bots close a position all at once. Partial TP scales out gradually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Dynamic split: 20% at first TP → up to 50% at final TP
&lt;/span&gt;&lt;span class="n"&gt;tp_levels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculate_partial_tp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;entry_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mark_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unrealized_pnl_pct&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# First fill: 20% of position, SL moves to breakeven
# Second fill: 30% more, trailing SL activates
# 50% left rides with trailing stop
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No numpy. Pure &lt;code&gt;statistics&lt;/code&gt; module. Works on any Python 3.11+ without extra dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trailing Stop for x10
&lt;/h3&gt;

&lt;p&gt;High-leverage positions move fast. The new &lt;code&gt;trailing_sl_x10&lt;/code&gt; module runs every heavy cycle, but &lt;strong&gt;only for positions with leverage ≥10&lt;/strong&gt;. It tightens the stop-loss when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;W-BB drops below 25% (price approaching lower band for LONG)&lt;/li&gt;
&lt;li&gt;PnL exceeds 15% (lock in profits)&lt;/li&gt;
&lt;li&gt;Update threshold: 0.5% from current mark price (avoids SL churn)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Auto Funding Rotation
&lt;/h3&gt;

&lt;p&gt;Negative funding rates eat your margin. The rotation module checks every heavy cycle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If any open position has funding rate below −0.01%: flag it&lt;/li&gt;
&lt;li&gt;If a better alternative exists (positive funding + BB signal): close current, open new&lt;/li&gt;
&lt;li&gt;Order matters: &lt;strong&gt;open first, then close&lt;/strong&gt; — never be out of the market during rotation&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The Telegram Bot That Survived Production
&lt;/h2&gt;

&lt;p&gt;The @Gridbolbot (formerly @GridSignalBot) is a 2,153-line Telegram bot that scans markets, sends alerts, and lets users execute trades inline. Five months in, it went silent.&lt;/p&gt;

&lt;p&gt;Not "slow" — dead silent. But the logs were clean, the process was running, systemd showed &lt;code&gt;active&lt;/code&gt;. Classic Heisenbug.&lt;/p&gt;

&lt;p&gt;I ran a &lt;strong&gt;3-echelon AI audit&lt;/strong&gt; — three agents in parallel, each with a different focus:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source-Driven:&lt;/strong&gt; code vs official documentation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security:&lt;/strong&gt; secrets, CVEs, command injection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adversarial:&lt;/strong&gt; race conditions, blocking calls, logic bugs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four minutes later: &lt;strong&gt;14 findings&lt;/strong&gt;. Five CRITICAL. Highlights:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;_valid_symbol()&lt;/code&gt; — called in 3 places, &lt;strong&gt;defined in zero&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Nine blocking &lt;code&gt;subprocess.run()&lt;/code&gt; calls inside async handlers&lt;/li&gt;
&lt;li&gt;Race condition in daily scan limit (&lt;code&gt;UPDATE ... SET +1&lt;/code&gt; without WHERE check)&lt;/li&gt;
&lt;li&gt;Ghost buttons in the keyboard with no handlers&lt;/li&gt;
&lt;li&gt;SQLite without WAL mode, database sitting in &lt;code&gt;.gitignore&lt;/code&gt; blind spot&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After fixes: &lt;strong&gt;45/45 smoke tests green&lt;/strong&gt;, bot responds instantly. Full story in the &lt;a href="https://dev.to/alexey_polyakov_cfe2095e3/i-built-an-ai-native-trading-engine-in-python-heres-how-it-works-54p0"&gt;separate article&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing: 45 Tests, Zero External Services
&lt;/h2&gt;

&lt;p&gt;Every fix, every feature, every release — the smoke test suite runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@pytest.mark.asyncio&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_scan_button_does_not_block_event_loop&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Verify scan handler yields control back to event loop.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;subprocess.run&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;mock_run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;mock_run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;return_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CompletedProcess&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
        &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;cmd_scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_race_condition_scan_limit&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Double-tap must not exceed daily limit.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE users SET scans_today = 9 WHERE id = 1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;cmd_scan&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;   &lt;span class="c1"&gt;# 10th — pass
&lt;/span&gt;    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RateLimitExceeded&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;cmd_scan&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;  &lt;span class="c1"&gt;# 11th — blocked
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tools: &lt;code&gt;pytest&lt;/code&gt;, &lt;code&gt;pytest-asyncio&lt;/code&gt;, &lt;code&gt;unittest.mock&lt;/code&gt;, SQLite &lt;code&gt;:memory:&lt;/code&gt; databases. No external services — pure deterministic tests in under 2 seconds.&lt;/p&gt;

&lt;p&gt;45 tests. 0 failures. CI green.&lt;/p&gt;




&lt;h2&gt;
  
  
  Numbers That Matter
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;v1 (Jan 2026)&lt;/th&gt;
&lt;th&gt;v2 (Jun 2026)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Strategies&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codebase&lt;/td&gt;
&lt;td&gt;2,100 lines&lt;/td&gt;
&lt;td&gt;4,600+ lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test coverage&lt;/td&gt;
&lt;td&gt;0 tests&lt;/td&gt;
&lt;td&gt;45 smoke tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ML layer&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;RandomForest F1=0.69&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backtesting&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Walk-forward on REST klines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Risk management&lt;/td&gt;
&lt;td&gt;Basic SL&lt;/td&gt;
&lt;td&gt;Partial TP + trailing x10 + funding rotation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Telegram bot&lt;/td&gt;
&lt;td&gt;Working, untested&lt;/td&gt;
&lt;td&gt;45/45 tests, 14 bugs fixed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;systemd + white-label script&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monitoring&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Prometheus /metrics + daily health alerts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;~200 MB&lt;/td&gt;
&lt;td&gt;~23.5 MB (SQLite beats in-memory dicts)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The memory drop isn't a typo. Moving from Python dicts to SQLite cut RAM by 88%.&lt;/p&gt;




&lt;h2&gt;
  
  
  Roadmap: Phase 4
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🔜 &lt;strong&gt;ATR-based risk sizing&lt;/strong&gt; — position size from volatility, not fixed&lt;/li&gt;
&lt;li&gt;🔜 &lt;strong&gt;Multi-timeframe confluence&lt;/strong&gt; — D/W/M agreement required for entries&lt;/li&gt;
&lt;li&gt;🔜 &lt;strong&gt;Grafana dashboard&lt;/strong&gt; — real-time PnL, drawdown, position heatmap&lt;/li&gt;
&lt;li&gt;🔜 &lt;strong&gt;Telegram Mini App&lt;/strong&gt; — dashboard right in Telegram&lt;/li&gt;
&lt;li&gt;🔮 &lt;strong&gt;OKX/Binance support&lt;/strong&gt; — same strategies, more liquidity&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/poliakarmai/bybit-ws
&lt;span class="nb"&gt;cd &lt;/span&gt;bybit-ws
&lt;span class="nb"&gt;cp &lt;/span&gt;config.example.yaml config.yaml
&lt;span class="c"&gt;# Insert your Bybit API keys&lt;/span&gt;
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
python &lt;span class="nt"&gt;-m&lt;/span&gt; bybit-ws
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or deploy as a systemd service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo cp &lt;/span&gt;bybit-ws.service /etc/systemd/system/
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; bybit-ws
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/poliakarmai/bybit-ws" rel="noopener noreferrer"&gt;poliakarmai/bybit-ws&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Bot:&lt;/strong&gt; &lt;a href="https://t.me/Gridbolbot" rel="noopener noreferrer"&gt;@Gridbolbot&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;License:&lt;/strong&gt; MIT&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The author is a trader and AI engineer. Writes about trading infrastructure, multi-agent systems, and the boring risk management that actually saves accounts.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>cryptocurrency</category>
      <category>trading</category>
      <category>ai</category>
    </item>
    <item>
      <title>Bybit-WS: AI-Native Trading Engine — как я построил автономного трейдингового агента за 8 месяцев</title>
      <dc:creator>Alexey Polyakov</dc:creator>
      <pubDate>Thu, 11 Jun 2026 14:18:29 +0000</pubDate>
      <link>https://dev.to/alexey_polyakov_cfe2095e3/bybit-ws-ai-native-trading-engine-kak-ia-postroil-avtonomnogho-trieidinghovogho-aghienta-za-8-miesiatsiev-47kg</link>
      <guid>https://dev.to/alexey_polyakov_cfe2095e3/bybit-ws-ai-native-trading-engine-kak-ia-postroil-avtonomnogho-trieidinghovogho-aghienta-za-8-miesiatsiev-47kg</guid>
      <description>&lt;h1&gt;
  
  
  Bybit-WS: AI-Native Trading Engine — как я построил автономного трейдингового агента за 8 месяцев
&lt;/h1&gt;

&lt;h2&gt;
  
  
  О чём речь
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;bybit-ws&lt;/strong&gt; — это не очередной торговый бот. Это движок, спроектированный для AI-агентов. Он даёт твоему Claude Code, Codex или Cursor REST API, MCP-сервер и Python SDK — и агент сам сканирует рынок, входит в позиции, управляет рисками. Без человека в цикле.&lt;/p&gt;

&lt;p&gt;Код: &lt;a href="https://github.com/poliakarmai/bybit-ws" rel="noopener noreferrer"&gt;github.com/poliakarmai/bybit-ws&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Торгует на Bybit с марта 2025. С июня 2026 — в Open Source. MIT.&lt;/p&gt;

&lt;h2&gt;
  
  
  Почему не freqtrade / hummingbot?
&lt;/h2&gt;

&lt;p&gt;Есть два класса трейдинговых инструментов:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Фреймворки&lt;/th&gt;
&lt;th&gt;bybit-ws&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Пишешь стратегию сам&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8 готовых стратегий из коробки&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP-сервер для AI-агентов&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Один YAML-конфиг&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Нет базы данных&lt;/td&gt;
&lt;td&gt;❌ (SQLite)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;freqtrade и hummingbot — фреймворки. Ты &lt;strong&gt;кодишь&lt;/strong&gt; стратегию. bybit-ws — готовый движок. Ты настраиваешь YAML, запускаешь, подключаешь AI-агента.&lt;/p&gt;

&lt;p&gt;Фреймворки хороши для исследователей. bybit-ws — для тех, кому нужно работать здесь и сейчас.&lt;/p&gt;

&lt;h2&gt;
  
  
  Архитектура
&lt;/h2&gt;

&lt;p&gt;Два слоя:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Интерфейсы (как AI-агенты общаются):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI Agent (Claude Code / Codex / Cursor / Hermes)
    ├── REST API (port 8766) — scan, enter, close, positions
    ├── MCP Server — scan_market, get_positions, get_metrics
    └── Python SDK — from bybit_ws_sdk import Monitor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Движок (30-секундный цикл, systemd или Docker):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;7 стратегий + DCA overlay&lt;/li&gt;
&lt;li&gt;Динамический сайзинг позиций (% от депозита × conviction score)&lt;/li&gt;
&lt;li&gt;Автоматический SL/TP через trading-stop&lt;/li&gt;
&lt;li&gt;X10 safety pack: ATR-валидация, дневные лимиты убытков, кулдауны&lt;/li&gt;
&lt;li&gt;Корреляционная матрица, фандинг-трекер, классификатор режима рынка&lt;/li&gt;
&lt;li&gt;SVG-дашборд и Telegram-бот @Gridbolbot&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7 стратегий
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Стратегия&lt;/th&gt;
&lt;th&gt;Плечо&lt;/th&gt;
&lt;th&gt;TF&lt;/th&gt;
&lt;th&gt;Триггер входа&lt;/th&gt;
&lt;th&gt;SL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;BB Grid LONG&lt;/td&gt;
&lt;td&gt;3x&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;BB &amp;lt; 25%, score ≥ 5.5&lt;/td&gt;
&lt;td&gt;−7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;BB Grid SHORT&lt;/td&gt;
&lt;td&gt;3x&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;BB &amp;gt; 85%, Tier A/B&lt;/td&gt;
&lt;td&gt;+5-7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Junk SHORT&lt;/td&gt;
&lt;td&gt;3x&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;Pump ≥ 80%&lt;/td&gt;
&lt;td&gt;−15%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;SL Re-entry&lt;/td&gt;
&lt;td&gt;3x&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;Лесенка после SL&lt;/td&gt;
&lt;td&gt;−7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;BB Scalp M5 ⚡&lt;/td&gt;
&lt;td&gt;10x&lt;/td&gt;
&lt;td&gt;M5&lt;/td&gt;
&lt;td&gt;Касание полосы + RSI&lt;/td&gt;
&lt;td&gt;3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Mean Revert ⚡&lt;/td&gt;
&lt;td&gt;10x&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;BB% &amp;lt; 5% или &amp;gt; 95%&lt;/td&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Funding Momentum ⚡&lt;/td&gt;
&lt;td&gt;10x&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;Funding ±0.1% + тренд&lt;/td&gt;
&lt;td&gt;4%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;⚡ — x10 плечо, ликвидация при ~10% движении. High risk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DCA overlay&lt;/strong&gt; — работает поверх всех стратегий: докупает на −5/−10/−15% от входа (до 2 добавок).&lt;/p&gt;

&lt;h2&gt;
  
  
  Как AI-агент торгует через bybit-ws
&lt;/h2&gt;

&lt;p&gt;Никакого ручного кликанья. Агент сам принимает решение:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bybit_ws_sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Monitor&lt;/span&gt;

&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Monitor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8766&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-rpc-token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Шаг 1: сканируем рынок
&lt;/span&gt;&lt;span class="n"&gt;signals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;long&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signals&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;7.0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Шаг 2: превью без исполнения (dry-run)
&lt;/span&gt;        &lt;span class="n"&gt;preview&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;symbol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Buy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qty&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;confirm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Маржа: $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;margin&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, Ликв: $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;liq_price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Шаг 3: вход с SL и TP
&lt;/span&gt;        &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;symbol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Buy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qty&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="n"&gt;sl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sl_suggested&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="n"&gt;tp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tp_suggested&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="n"&gt;confirm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;И всё. Дальше bybit-ws сам следит за позицией: SL, TP, авто-закрытие при деградации BB, DCA на откатах.&lt;/p&gt;

&lt;h2&gt;
  
  
  Risk Management
&lt;/h2&gt;

&lt;p&gt;Потому что без защиты на фьючерсах делать нечего:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Динамический сайзинг&lt;/strong&gt;: депозит × risk% / max_positions × conviction score. Больше уверенность → больше позиция&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drawdown guard&lt;/strong&gt;: алерт + опциональный emergency close при −15% от пика депозита&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Дневной лимит убытков&lt;/strong&gt;: стоп торговли при −$N за день&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Корреляционный блок&lt;/strong&gt;: запрет входа если ≥2 позиции коррелируют &amp;gt;0.8&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X10 limits&lt;/strong&gt;: макс 3 убыточных x10 сделки → кулдаун 24 часа&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Каскадная защита&lt;/strong&gt;: market-close если цена в 2× ближе к ликвидации чем SL&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Безопасность
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;API-ключи через переменные окружения (&lt;code&gt;${BYBIT_API_KEY}&lt;/code&gt;), никогда в коде&lt;/li&gt;
&lt;li&gt;RPC auth: Bearer token на все write-эндпоинты (&lt;code&gt;/enter&lt;/code&gt;, &lt;code&gt;/close&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Bind 127.0.0.1 по умолчанию — не светим наружу&lt;/li&gt;
&lt;li&gt;chmod 600 на конфиг&lt;/li&gt;
&lt;li&gt;Рекомендация: IP whitelist на стороне Bybit&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Запуск за 5 минут
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/poliakarmai/bybit-ws.git
&lt;span class="nb"&gt;cd &lt;/span&gt;bybit-ws
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;                    &lt;span class="c"&gt;# ставит bybit-ws + зависимости&lt;/span&gt;
&lt;span class="nb"&gt;cp &lt;/span&gt;config.example.yaml ~/.config/bybit-ws/config.yaml
&lt;span class="c"&gt;# Вставь API-ключи и НАЧНИ С ТЕСТНЕТА:&lt;/span&gt;
&lt;span class="c"&gt;#   api.base_url: "https://api-testnet.bybit.com"&lt;/span&gt;
bybit-ws daemon
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Всё. Демон запущен, REST API на порту 8766. Здоровье: &lt;code&gt;curl localhost:8766/health&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Что под капотом
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python 3.11+&lt;/strong&gt;, 8500+ строк, 37 модулей&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Скоринг&lt;/strong&gt;: 9 метрик (Tier, BB Daily, Volume, Days Falling, Weekly/Monthly BB, Funding, Volatility, Bounce Quality, RSI)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Режимы рынка&lt;/strong&gt;: классификатор определяет тренд/рэндж/волатильность и адаптирует параметры&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Фандинг-трекер&lt;/strong&gt;: исторические ставки, прогноз, фильтр входа при минусовом фандинге&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SVG-дашборд&lt;/strong&gt;: винрейт, маржа, фандинг, корреляции — обновляется раз в 5 минут&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Telegram-бот @Gridbolbot&lt;/strong&gt;: живые сигналы, алерты, утренняя сводка&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Production Track Record
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Самоотчёт с продакшена. Не аудировано. Прошлые результаты ≠ будущие.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Аптайм&lt;/strong&gt;: 99.7% (30 дней)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Сделок&lt;/strong&gt;: 847 за 30 дней&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Винрейт (LONG)&lt;/strong&gt;: ~68%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Среднее удержание&lt;/strong&gt;: 14 часов&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Roadmap
&lt;/h2&gt;

&lt;p&gt;Следующие шаги:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WebSocket миграция&lt;/strong&gt; — замена REST-поллинга на реальный поток&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Бэктестинг&lt;/strong&gt; — прогон стратегий на исторических данных&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-exchange&lt;/strong&gt; — Binance, OKX&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Веб-дашборд&lt;/strong&gt; — реальный UI вместо SVG&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PR'ы приветствуются. См. &lt;a href="https://github.com/poliakarmai/bybit-ws#-contributing" rel="noopener noreferrer"&gt;CONTRIBUTING&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Итог
&lt;/h2&gt;

&lt;p&gt;Я построил этот движок чтобы мой AI-агент (Hermes) мог торговать автономно. Получилось так, что он подходит любому агенту — Claude Code, Codex, Cursor. 8 месяцев разработки, из них 6 в проде на реальных деньгах.&lt;/p&gt;

&lt;p&gt;Если ты:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI-разработчик&lt;/strong&gt; и хочешь чтобы твой агент торговал&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Квант-трейдер&lt;/strong&gt; которому нужен API для Bollinger Grid&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Крипто-энтузиаст&lt;/strong&gt; который хочет 24/7 мониторинг без кодинга&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;— клонируй, ставь тестнет, пробуй. Бесплатно. MIT.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/poliakarmai/bybit-ws" rel="noopener noreferrer"&gt;github.com/poliakarmai/bybit-ws&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Telegram-бот&lt;/strong&gt;: &lt;a href="https://t.me/Gridbolbot" rel="noopener noreferrer"&gt;@Gridbolbot&lt;/a&gt; — демо, 10 сканов/день бесплатно&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Канал&lt;/strong&gt;: &lt;a href="https://t.me/criptapolyaka" rel="noopener noreferrer"&gt;@criptapolyaka&lt;/a&gt; — сигналы и разбор рынка&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bybit&lt;/strong&gt;: &lt;a href="https://www.bybit.com/invite?ref=DQ0EAQ&amp;amp;medium=referral&amp;amp;utm_campaign=evergreen" rel="noopener noreferrer"&gt;Регистрация с $30 бонусом&lt;/a&gt; — поддерживает проект&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;⚠️ &lt;strong&gt;Дисклеймер&lt;/strong&gt;: Торговля фьючерсами с плечом — высокий риск. Можно потерять весь депозит. Это не финансовая рекомендация. Начинайте с тестнета.&lt;/p&gt;

</description>
      <category>python</category>
      <category>crypto</category>
      <category>trading</category>
      <category>ai</category>
    </item>
    <item>
      <title>How I Fixed a Silent Telegram Bot Using a 3-Echelon AI Audit (14 Bugs Found)</title>
      <dc:creator>Alexey Polyakov</dc:creator>
      <pubDate>Wed, 10 Jun 2026 10:04:06 +0000</pubDate>
      <link>https://dev.to/alexey_polyakov_cfe2095e3/i-built-an-ai-native-trading-engine-in-python-heres-how-it-works-54p0</link>
      <guid>https://dev.to/alexey_polyakov_cfe2095e3/i-built-an-ai-native-trading-engine-in-python-heres-how-it-works-54p0</guid>
      <description>&lt;h1&gt;
  
  
  How I Fixed a Silent Telegram Bot Using a 3-Echelon AI Audit (14 Bugs Found)
&lt;/h1&gt;

&lt;p&gt;My Telegram bot stopped responding.&lt;/p&gt;

&lt;p&gt;Not "responding slowly" — just dead silent. You tap the "Scan" button and nothing comes back. The process is running, memory's fine, logs are clean. Classic Heisenbug: it breaks when you're not watching, works when you are.&lt;/p&gt;

&lt;p&gt;I'm an engineer with a background in industrial safety (pipeline diagnostics, corrosion monitoring), but for the past six months I've been deep in AI agents and trading infrastructure. Here's what I learned: debugging with AI isn't just "ask ChatGPT to fix the error." It's a systematic approach.&lt;/p&gt;

&lt;p&gt;Let me show you how three AI agents scanned 2,153 lines of code in parallel, found 14 bugs of varying nastiness, and brought the bot back to life.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bot That Went Silent
&lt;/h2&gt;

&lt;p&gt;Context first. The bot is called GridSignal — a trading tool for Bybit futures. It scans the market using Bollinger Bands, generates entry signals, sends alerts. 2,153 lines of Python, a bunch of dependencies, its own SQLite database, subprocess calls to the Bybit CLI. Your typical "grown-up" Telegram bot.&lt;/p&gt;

&lt;p&gt;The problem surfaced after adding a new feature: funding rate rotation. I plugged a call to &lt;code&gt;funding_rotation.py&lt;/code&gt; into the "Rotation" button handler, and the bot went down. Not immediately — first it just got sluggish, then stopped responding entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Silent Killer: Async Exceptions Without Logging
&lt;/h3&gt;

&lt;p&gt;The real kicker: logs were spotless. The process showed &lt;code&gt;active (running)&lt;/code&gt; in systemd. I spent an hour guessing — "maybe Telegram API is having issues? network? self-healing?" — until I ran the audit.&lt;/p&gt;

&lt;p&gt;Here's why the logs were clean: asyncio has a nasty habit of &lt;strong&gt;swallowing exceptions silently&lt;/strong&gt;. If a coroutine raises inside a handler that doesn't have its own try/except, and nobody's awaiting it, the exception gets logged to the event loop's default handler — which, by default, prints to stderr and nothing else. Your logging framework never sees it.&lt;/p&gt;

&lt;p&gt;The fix is a global exception handler that plugs into your logging pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_event_loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_exception_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Global asyncio error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;exc_info&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;exception&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this, every swallowed exception shows up in your logs — no more Heisenbugs hiding in the shadows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Echelons
&lt;/h2&gt;

&lt;p&gt;I run my own AI agent platform called Hermes. It can spawn multiple auditors in parallel, each with a different focus. The architecture looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 ┌─────────────┐
                 │  Your Code  │
                 └──────┬──────┘
          ┌─────────────┼─────────────┐
          ▼             ▼             ▼
  ┌──────────────┐ ┌──────────┐ ┌──────────────┐
  │Source-Driven │ │ Security  │ │ Adversarial  │
  │              │ │           │ │              │
  │ API docs     │ │ Secrets &amp;amp; │ │ Race cond.   │
  │ vs code      │ │ CVEs      │ │ Logic bugs   │
  └──────┬───────┘ └─────┬─────┘ └──────┬───────┘
         └───────────────┼──────────────┘
                         ▼
              ┌──────────────────┐
              │  Consolidated    │
              │  14 Findings     │
              └──────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source-Driven:&lt;/strong&gt; cross-references code against official documentation. Finds API calls that don't exist, parameters that aren't supported, hallucinations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security:&lt;/strong&gt; hunts for secrets in code, holes in &lt;code&gt;.gitignore&lt;/code&gt;, command injection, CVEs in dependencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adversarial:&lt;/strong&gt; finds fatal bugs in business logic. Race conditions, blocking calls, resource leaks, edge cases.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why three instead of one? Because they have different blind spots. A security auditor will catch &lt;code&gt;subprocess.run&lt;/code&gt; with f-strings beautifully, but won't notice that &lt;code&gt;_valid_symbol()&lt;/code&gt; is never defined anywhere. Source-driven will find doc mismatches, but will miss a race condition in scan limits. Adversarial will spot 10 sequential subprocess calls hanging the event loop for 100 seconds — but it doesn't care about CVEs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prompts That Drove the Audit
&lt;/h3&gt;

&lt;p&gt;What exactly did I ask the agents? Here are the actual prompts that anyone can use with ChatGPT, Claude, or Cursor:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adversarial echelon prompt:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Analyze this Telegram bot handler code. Find every place where a race condition is possible — if the user taps the button twice within 100ms, or two users call this function simultaneously. Also flag all blocking calls (subprocess.run, time.sleep, file I/O) inside async handlers. For each finding, suggest an atomic fix. Prioritize by severity."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Source-Driven echelon prompt:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Cross-reference every API call in this code against official python-telegram-bot documentation and Bybit API v5 docs. Flag: (1) parameters not documented, (2) deprecated methods, (3) return values used incorrectly. Include links to the relevant docs for each finding."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Three agents, parallel execution. Four minutes later, I had the consolidated report: 14 findings. Five CRITICAL, four HIGH.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Found
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The Function That Doesn't Exist
&lt;/h3&gt;

&lt;p&gt;The bot calls &lt;code&gt;_valid_symbol(symbol)&lt;/code&gt; in three places: during &lt;code&gt;/alert&lt;/code&gt;, during background alert checking, and during inline queries (&lt;code&gt;@Gridbolbot BTCUSDT&lt;/code&gt;). But the function is never defined. Not in the main file, not in any import, not in any adjacent module.&lt;/p&gt;

&lt;p&gt;When a user tapped &lt;code&gt;/alert BTCUSDT&lt;/code&gt;, the bot crashed with &lt;code&gt;NameError: name '_valid_symbol' is not defined&lt;/code&gt;. The handler died, no alert was set, and I sat there scratching my head at the clean logs.&lt;/p&gt;

&lt;p&gt;The Source-Driven echelon found this. Interestingly, the Security echelon also found &lt;code&gt;_valid_symbol&lt;/code&gt; — but from a different angle: "symbol validation function undefined, potential command injection via subprocess if it existed."&lt;/p&gt;

&lt;p&gt;Two echelons, two different reasons to worry about the same non-existent function.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Nine Blocking Calls in Async
&lt;/h3&gt;

&lt;p&gt;The Adversarial echelon walked through every handler and built a table. The worst offender: &lt;code&gt;cmd_fear&lt;/code&gt;. The "Fear" button made 10 sequential &lt;code&gt;subprocess.run(['bybit', 'bb', ...])&lt;/code&gt; calls. Each with a 10-second timeout. Ten coins, ten calls, 100 seconds of event-loop blockage.&lt;/p&gt;

&lt;p&gt;Async Python doesn't forgive this. While &lt;code&gt;cmd_fear&lt;/code&gt; waits for Bybit's response on the tenth coin, every other user sees "typing..." and gets nothing. If two people hit buttons at the same time — the bot just freezes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# BLOCKS the entire event loop for up to 100 seconds
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;symbols&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bybit&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;parse_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Event loop stays free — other users can still interact
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;symbols&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bybit&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;parse_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three lines changed, event loop freed.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Race Condition in Scan Limits
&lt;/h3&gt;

&lt;p&gt;The bot has a limit: 10 scans per user per day. Abuse protection.&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;update_scan_count()&lt;/code&gt; blindly did &lt;code&gt;UPDATE users SET scans_today=scans_today+1&lt;/code&gt; without checking the current value. If a user managed to fire &lt;code&gt;/scan&lt;/code&gt; twice in one event-loop tick (fast double tap), both requests saw &lt;code&gt;scans_today=0&lt;/code&gt; and both passed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Two concurrent taps → both see scans_today=0 → both pass
&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE users SET scans_today = scans_today + 1 WHERE user_id = ?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After — atomic check-and-increment:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;UPDATE users SET scans_today = scans_today + 1
       WHERE user_id = ? AND scans_today &amp;lt; 10&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rowcount&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RateLimitExceeded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Daily scan limit reached&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No more race. The database itself enforces the constraint.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Ghost Buttons and Duplicates
&lt;/h3&gt;

&lt;p&gt;The "📊 LONG" and "📉 SHORT" buttons were in the keyboard, but nobody handled the press. Users tapped them — zero response. The Adversarial echelon flagged it: "buttons exist in &lt;code&gt;MAIN_KEYBOARD&lt;/code&gt;, no handler implemented."&lt;/p&gt;

&lt;p&gt;We also found a &lt;code&gt;cmd_top&lt;/code&gt; duplicate: the command was registered as a handler twice, and inside &lt;code&gt;cmd_stats&lt;/code&gt; there was another &lt;code&gt;cmd_top&lt;/code&gt; (leaderboard) silently overwriting the original (top gainers/losers). Split into separate functions, cleaned up.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. SQLite Without WAL, and the Database Sitting in Git
&lt;/h3&gt;

&lt;p&gt;The Security echelon found that &lt;code&gt;.gitignore&lt;/code&gt; didn't exclude &lt;code&gt;data/*.db&lt;/code&gt; — SQLite files with user data could accidentally fly into the repository. They hadn't, but they could have.&lt;/p&gt;

&lt;p&gt;Another echelon noticed: &lt;code&gt;sqlite3.connect()&lt;/code&gt; without &lt;code&gt;check_same_thread=False&lt;/code&gt; and without &lt;code&gt;PRAGMA journal_mode=WAL&lt;/code&gt;. Two concurrent users and you get &lt;code&gt;SQLITE_BUSY: database is locked&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;data/bot.db&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;data/bot.db&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_same_thread&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PRAGMA journal_mode=WAL;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two extra parameters, zero more locking errors.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Temp File Leak
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;/chart&lt;/code&gt; command generates a chart image, saves it temporarily, sends it to the user, and deletes it. Simple — unless the chart generation raises an exception.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;mpf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;savefig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;chart.png&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;chart.png&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reply_photo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;chart.png&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Never runs if mpf.plot() raises!
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;NamedTemporaryFile&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;NamedTemporaryFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;suffix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.png&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delete&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;mpf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;savefig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reply_photo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;# File is auto-deleted by the context manager — even on exceptions
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tempfile.NamedTemporaryFile&lt;/code&gt; guarantees cleanup. The OS handles it, not your error-prone manual &lt;code&gt;os.unlink()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Else We Found (Less Dramatic)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;parse_mode='Markdown'&lt;/code&gt; instead of &lt;code&gt;MarkdownV2&lt;/code&gt; in three places. Old mode, deprecated. Doesn't break things, but it's an eyesore.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;requirements.txt&lt;/code&gt; incomplete — three libraries missing. Deploy on a clean machine and you won't know what's missing.&lt;/li&gt;
&lt;li&gt;Bare &lt;code&gt;import re&lt;/code&gt; missing, even though &lt;code&gt;_valid_symbol()&lt;/code&gt; (once we actually wrote it) used &lt;code&gt;re.fullmatch()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Twenty-one &lt;code&gt;except:&lt;/code&gt; clauses without exception type — swallowing KeyboardInterrupt and SystemExit along with real errors.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Testing: How 45/45 Smoke Tests Passed Green
&lt;/h2&gt;

&lt;p&gt;After every fix, I ran the smoke test suite. Here's the setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pytest with asyncio support, async fixtures, mocks
# test_smoke.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;unittest.mock&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AsyncMock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;patch&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.mark.asyncio&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_scan_button_does_not_block_event_loop&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Verify scan handler yields control back to event loop.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;subprocess.run&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;mock_run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;mock_run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;return_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CompletedProcess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="n"&gt;returncode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;cmd_scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;  &lt;span class="c1"&gt;# Should return fast — actual work is in to_thread()
&lt;/span&gt;
&lt;span class="nd"&gt;@pytest.mark.asyncio&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_race_condition_scan_limit&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Double-tap scan must not exceed daily limit.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE users SET scans_today = 9 WHERE user_id = 1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;cmd_scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 10th scan — should pass
&lt;/span&gt;    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RateLimitExceeded&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;cmd_scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 11th — blocked
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tools used: &lt;code&gt;pytest&lt;/code&gt;, &lt;code&gt;pytest-asyncio&lt;/code&gt;, &lt;code&gt;unittest.mock&lt;/code&gt; for Telegram API calls, &lt;code&gt;sqlite3&lt;/code&gt; with &lt;code&gt;:memory:&lt;/code&gt; databases for isolation. No external services touched — pure deterministic tests that run in under 2 seconds.&lt;/p&gt;

&lt;p&gt;45 tests, 0 failures. CI green.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;One.&lt;/strong&gt; A single auditor is dangerous. It will confidently say "all clear" because it's looking from one angle. Three agents with different focuses cover each other's blind spots.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two.&lt;/strong&gt; Blocking calls in async handlers aren't a "fix later" thing. They make your bot look dead in production. &lt;code&gt;subprocess.run()&lt;/code&gt; inside async is a red flag. Always.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three.&lt;/strong&gt; You can't spot a race condition by eyeballing it — especially in code you wrote yourself. You need an adversarial echelon that actively asks "what if they tap twice, fast?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Four.&lt;/strong&gt; Buttons in a keyboard without handlers are negligence you never notice because you never use those buttons. Users do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Five.&lt;/strong&gt; Type annotations double your auditor's accuracy. An AI agent that sees &lt;code&gt;def handler(update: Update, context: ContextTypes.DEFAULT_TYPE)&lt;/code&gt; makes 30–40% fewer false positives than one seeing &lt;code&gt;def handler(update, context)&lt;/code&gt;. Add type hints — your tools get smarter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: What Else to Check in Your Telegram Bot
&lt;/h2&gt;

&lt;p&gt;Beyond the five bugs above, here's a checklist of things that bite bots in production. Run through these — each one takes 2 minutes to verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Rate Limiting (429 Too Many Requests).&lt;/strong&gt; Does your bot handle Telegram API backpressure? Use aiogram's built-in retry middleware or add exponential backoff: &lt;code&gt;await asyncio.sleep(min(2 ** attempt, 60))&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Type Hints.&lt;/strong&gt; Run &lt;code&gt;mypy --strict&lt;/code&gt; on your handlers. AI auditors get 30–40% more accurate with typed code — the model understands variable context better.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Global asyncio exception handler.&lt;/strong&gt; Add the &lt;code&gt;loop.set_exception_handler()&lt;/code&gt; snippet from above. Silent exceptions are the #1 cause of "it works on my machine" Heisenbugs.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Atomic database operations.&lt;/strong&gt; Any counter, limit, or state transition that can be triggered by concurrent users needs &lt;code&gt;WHERE ... AND current_state &amp;lt; limit&lt;/code&gt; — never two-step read-then-write.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Temporary file cleanup.&lt;/strong&gt; Replace all manual &lt;code&gt;os.unlink()&lt;/code&gt; with &lt;code&gt;tempfile.NamedTemporaryFile&lt;/code&gt; or &lt;code&gt;try/finally&lt;/code&gt;. One exception and your disk fills up.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Dependency completeness.&lt;/strong&gt; Run &lt;code&gt;pip freeze &amp;gt; requirements.txt&lt;/code&gt; from a clean venv, not your dev environment. Three missing libraries is typical.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;After all fixes: 0 CRITICAL, 45/45 smoke tests green, bot responds instantly. Audit time: 4 minutes. Manual debugging time: 1 hour.&lt;/p&gt;

&lt;p&gt;I'm not saying AI audits replace code review. But as a first line of defense, they're terrifyingly effective — especially when your project crosses 2,000 lines and keeping it all in your head is simply not realistic.&lt;/p&gt;

&lt;p&gt;Got a Telegram bot that "sometimes lags"? Try running the three-echelon approach with the prompts above. Chances are, you'll find a couple of your own &lt;code&gt;_valid_symbol&lt;/code&gt;s.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The author is a trader and AI engineer. Writes about trading bot infrastructure, multi-agent systems, and practical production debugging.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>telegram</category>
      <category>ai</category>
      <category>debugging</category>
    </item>
  </channel>
</rss>
