Disclaimer: This article is for educational purposes only. It does not constitute financial advice. Always do your own research and comply with your local regulations before trading.
Crypto signal services charge $40-55/month. You open Telegram, get a daily "BUY" or "SELL", and hope for the best. Ask them how the signal is generated? Silence. Ask to see the backtest data? Nothing.
I was already running a trading bot. Python, ccxt, the usual stack. One day I looked at the signals my bot was producing and thought — wait, this is a signal service. I just wasn't sending it anywhere.
So I wired it up to Telegram. Total cost: $0. Win rate: 35%. And it's profitable.
Here's how.
Why I Built This
I started with a simple EMA crossover bot trading BTC/USDT on Bitget. $33 starting capital. It worked, but I wanted to know if there was something better.
So I built a backtesting engine and ran 50 strategies against 37 months of BTC/USDT daily data (Jan 2023 – Feb 2026). Every single result — Sharpe ratio, max drawdown, win rate, trade count — is public on GitHub. No cherry-picking.
The top 3 strategies became my signal sources:
| Strategy | What It Does | Sharpe |
|---|---|---|
| EMA Crossover | Trades when short-term and long-term moving averages cross | 1.30 |
| Parabolic SAR | Detects trend reversals | 1.25 |
| MACD | Measures trend strength and direction | 1.17 |
Each strategy independently generates BUY/SELL/HOLD. When 2 out of 3 agree, that becomes the consensus signal. Simple majority vote.
This is where it clicked. The paid signal services? They're doing the same thing — pulling data from an exchange, running it through some strategy, and pushing the result to Telegram. There's no secret sauce. The difference is whether you can see what's inside.
I wanted to see what's inside. So I built my own.
Architecture
The whole system runs on a daily cron job. No cloud, no Docker, just a Python script.
cron (daily, 00:15 UTC)
│
▼
Bitget API → fetch OHLCV data (via ccxt)
│
▼
3 strategies analyze independently
│
▼
Majority vote → BUY / SELL / HOLD
│
▼
Telegram Bot API → push to channel
The signal module lives in src/signal/ with a clean separation:
src/signal/
├── config.py # Environment config (frozen dataclass)
├── generator.py # Runs all 3 strategies, produces consensus
├── formatter.py # Formats messages (free vs premium)
├── sender.py # Sends to Telegram (async, retries, dry_run)
├── notifier.py # Orchestrates the flow
└── main.py # Entry point for cron
The free channel gets the consensus signal. The premium channel gets the full breakdown — individual strategy outputs, indicator values, circuit breaker status.
Free channel output:
🟢 BTC/USDT Signal
Consensus: BUY
Price: $96,543
Full strategy breakdown → Premium channel
Premium channel output:
🟢 BTC/USDT Signal Alert
Consensus: BUY (2/3 strategies agree)
Price: $96,543.21
Strategy Breakdown:
🟢 EMA Crossover: BUY
EMA(12)=96800 / EMA(26)=96200
🟢 MACD: BUY
MACD=245 Signal=180
🟡 Parabolic SAR: HOLD
SAR=95100 trend=UP
CB Status: NORMAL
Time: 2026-03-28 00:15 UTC
The SignalGenerator class handles the consensus logic:
def generate(self, ohlcv: pd.DataFrame) -> AggregatedSignal:
signals = [
self._ema_crossover(ohlcv),
self._parabolic_sar(ohlcv),
self._macd(ohlcv),
]
buy_count = sum(1 for s in signals if s.direction == "BUY")
sell_count = sum(1 for s in signals if s.direction == "SELL")
if buy_count >= 2:
consensus = "BUY"
elif sell_count >= 2:
consensus = "SELL"
else:
consensus = "HOLD"
return AggregatedSignal(
consensus=consensus,
strategies=signals,
)
Nothing fancy. Three strategies vote, majority wins. If EMA and MACD both say BUY but SAR says HOLD, the consensus is BUY. When all three agree, that's a high-conviction signal.
Key Code: The Telegram Integration
The Telegram side is surprisingly simple. python-telegram-bot does the heavy lifting.
from telegram import Bot
class TelegramSender:
def __init__(self, bot_token: str, channel_id: str, dry_run: bool = True):
self._bot = Bot(token=bot_token)
self._channel_id = channel_id
self._dry_run = dry_run
async def send(self, message: str) -> None:
if self._dry_run:
print(f"[DRY_RUN] Would send to {self._channel_id}:\n{message}")
return
await self._bot.send_message(
chat_id=self._channel_id,
text=message,
parse_mode="Markdown",
)
Setting up the bot took 3 minutes:
- Message
@BotFatheron Telegram →/newbot - Get the bot token
- Create a channel, add the bot as admin
- Drop the token and channel ID into
.env
TELEGRAM_BOT_TOKEN=your_token_here
TELEGRAM_FREE_CHANNEL_ID=-100xxxxxxxxxx
SIGNAL_DRY_RUN=true
I ran it with DRY_RUN=true first, confirmed the output format looked right, then flipped it to false. The whole pipeline — data fetch, strategy analysis, consensus, Telegram delivery — runs in under 10 seconds.
Honest Numbers: Why 35% Win Rate Still Works
This is the part most people get stuck on.
"35% win rate means you lose 7 out of 10 trades. How is that profitable?"
The answer: risk-reward ratio.
When the bot loses, it loses small. The stop-loss is tight. When it wins, it wins big. The take-profit is set at 3x+ the stop-loss distance.
Here's what that looks like over 10 trades:
7 losses × -$1.00 = -$7.00
3 wins × +$3.50 = +$10.50
─────────────────────────────
Net: +$3.50
This is expected value at work. Win rate alone tells you nothing. A strategy with 80% win rate can still lose money if the wins are tiny and the losses are huge. And a 35% win rate strategy can print money if the risk-reward ratio is favorable.
My backtest data across 37 months confirms this pattern holds. The EMA crossover strategy specifically returned 491% with a Sharpe ratio of 1.30 — at a 35% win rate.
But I'll be honest about the rough patches. Right after going live, I hit 8 consecutive losses. That's statistically normal for a 35% win rate system, but it doesn't feel normal when you're watching it happen. The strategy wasn't broken — it was just a ranging market. When the trend came back, so did the profits.
The mental game is the hardest part of running a low-win-rate system. You have to trust the math even when your gut says to pull the plug.
Here's the key backtest comparison:
| Metric | EMA Crossover | Parabolic SAR | MACD |
|---|---|---|---|
| Return | 491% | 456% | 428% |
| Sharpe | 1.30 | 1.25 | 1.17 |
| Win Rate | 35% | 36% | 36% |
| Max Drawdown | -34% | -37% | -33% |
| Trades | 34 | 94 | 84 |
None of these have a win rate above 36%. All three are profitable over 37 months. The common thread: they cut losses early and let winners run.
What's Next
The bot is live. The free Telegram channel is running. Every day, BTC/USDT consensus signals go out.
What I'm working on now:
- Multi-pair expansion — Adding ETH/USDT and SOL/USDT to the signal feed. More pairs = more signals = more useful data for subscribers
- Premium channel launch — Full strategy breakdowns, individual indicator values, circuit breaker status. Targeting $29/month
- Walk-forward optimization — Continuously re-evaluating strategy parameters against recent data instead of static backtests
The whole thing is open source. You can read the strategy code, run the backtests yourself, and verify every number I've shared here.
Building this taught me something. Signal services aren't magic. They're a data pipeline with a Telegram endpoint. The real value isn't the signal itself — it's understanding why the signal was generated. When you build it yourself, you get that for free.
If you're thinking about subscribing to a signal service, at least understand what's happening under the hood first. And if you know some Python, maybe just build your own.
The win rate is 35%. The math still works.
Top comments (0)