9 strategies β 12. ML scoring, backtesting, partial take-profit, Telegram bot that survived a 3-echelon audit. Open source, MIT, trading real money.
Trading bots come in two flavors: black-box SaaS at $50/month, or GitHub scripts that crash at 3 AM. I wanted neither.
Five months ago I shipped v1 of bybit-ws β 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.
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.
v1 was clean. v2 is battle-tested.
ββββββββββββββββββββββββββββ
β 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 β
ββββββββββββββββββββββββββββββ
β = new in v2
Three cycles instead of two. The new x10 cycle (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.
State is SQLite, period. 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.
| Strategy | Lev | Timeframe | What's New |
|---|---|---|---|
| Bollinger Grid LONG | 3x | Daily | β |
| Bollinger Grid SHORT | 3x | Daily | β |
| Junk Short | 3x | Daily | β |
| SL Re-entry | 3x | Daily | β |
| DCA Ladder | 3x | β | β |
| BB Scalping | 10x | M5 | β |
| Mean Reversion | 10x | Daily | β |
| Funding Momentum | 10x | Daily | β |
| ATR Risk Sizing | layer | 15m | β |
| Partial TP | |||
| β | dynamic | 20β50% scale-out, no numpy | |
| Trailing SL x10 | |||
| β | x10 only | Tight trailing on high-lev positions | |
| Funding Rotation | |||
| β | auto | Closes positions before negative funding hits |
The golden rule still applies: every entry passes through scoring across 8+ metrics with a 5.5/10 threshold. But now there's a new layer on top.
After 5 months, the system had logged 282 real signals with outcomes β entry price, exit price, PnL, whether it hit take-profit or stop-loss. That's a dataset.
I trained a RandomForest classifier on it:
features = ['score', 'price_vs_lower', 'price_vs_upper',
'volume_24h', 'funding_rate', 'rsi_14',
'bb_squeeze', 'consecutive_down_days']
target = 'is_profitable' # 1 if PnL > 0, else 0
Results:
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 why it made a decision.
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.
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:
for day in trading_days:
klines = fetch_klines(symbol, start=day.start_ms)
signals = scan(klines, strategy='bollinger_grid')
for sig in signals:
trade = simulate(sig, klines[day:day+30])
results.append(trade)
Tested on BTC, ETH, SUI, and ADA with Daily signals:
| Symbol | Win Rate | Avg PnL | Best Trade | Worst Trade |
|---|---|---|---|---|
| SUIUSDT | 42.9% | +6.56% | +27.3% | β12.1% |
| ADAUSDT | 38.5% | +4.82% | +19.4% | β11.7% |
| BTCUSDT | 35.7% | +3.11% | +14.2% | β9.3% |
| ETHUSDT | 33.3% | +1.95% | +11.8% | β10.5% |
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.
Most bots close a position all at once. Partial TP scales out gradually:
tp_levels = calculate_partial_tp(
entry_price, mark_price, unrealized_pnl_pct
)
No numpy. Pure statistics
module. Works on any Python 3.11+ without extra dependencies.
High-leverage positions move fast. The new trailing_sl_x10
module runs every heavy cycle, but only for positions with leverage β₯10. It tightens the stop-loss when:
Negative funding rates eat your margin. The rotation module checks every heavy cycle:
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.
Not "slow" β dead silent. But the logs were clean, the process was running, systemd showed active
. Classic Heisenbug.
I ran a 3-echelon AI audit β three agents in parallel, each with a different focus:
Four minutes later: 14 findings. Five CRITICAL. Highlights:
_valid_symbol()
β called in 3 places, subprocess.run()
calls inside async handlersUPDATE ... SET +1
without WHERE check).gitignore
blind spotAfter fixes: 45/45 smoke tests green, bot responds instantly. Full story in the separate article.
Every fix, every feature, every release β the smoke test suite runs:
@pytest.mark.asyncio
async def test_scan_button_does_not_block_event_loop():
"""Verify scan handler yields control back to event loop."""
with patch('subprocess.run') as mock_run:
mock_run.return_value = CompletedProcess(...)
start = time.monotonic()
await cmd_scan(update, context)
assert time.monotonic() - start < 0.5
def test_race_condition_scan_limit():
"""Double-tap must not exceed daily limit."""
db.execute("UPDATE users SET scans_today = 9 WHERE id = 1")
await cmd_scan(...) # 10th β pass
with pytest.raises(RateLimitExceeded):
await cmd_scan(...) # 11th β blocked
Tools: pytest
, pytest-asyncio
, unittest.mock
, SQLite :memory:
databases. No external services β pure deterministic tests in under 2 seconds.
45 tests. 0 failures. CI green.
| Metric | v1 (Jan 2026) | v2 (Jun 2026) |
|---|---|---|
| Strategies | 9 | 12 |
| Codebase | 2,100 lines | 4,600+ lines |
| Test coverage | 0 tests | 45 smoke tests |
| ML layer | None | RandomForest F1=0.69 |
| Backtesting | None | Walk-forward on REST klines |
| Risk management | Basic SL | Partial TP + trailing x10 + funding rotation |
| Telegram bot | Working, untested | 45/45 tests, 14 bugs fixed |
| Deployment | Manual | systemd + white-label script |
| Monitoring | None | Prometheus /metrics + daily health alerts |
| Memory | ~200 MB | ~23.5 MB (SQLite beats in-memory dicts) |
The memory drop isn't a typo. Moving from Python dicts to SQLite cut RAM by 88%.
git clone https://github.com/poliakarmai/bybit-ws
cd bybit-ws
cp config.example.yaml config.yaml
pip install -r requirements.txt
python -m bybit-ws
Or deploy as a systemd service:
sudo cp bybit-ws.service /etc/systemd/system/
sudo systemctl enable --now bybit-ws
GitHub: poliakarmai/bybit-ws
Bot: @Gridbolbot
License: MIT
The author is a trader and AI engineer. Writes about trading infrastructure, multi-agent systems, and the boring risk management that actually saves accounts.