Automated crypto signal engine that analyses closed daily, 4-hour, and 15-minute candles and delivers calibrated BUY / SELL / NO ACTION decisions to Telegram.
Built for traders who want actionable signals, backtested context, and zero manual babysitting.
- Multi-timeframe probabilistic ensemble using RSI, MACD, EMA, ADX, SuperTrend, VWAP, Bollinger, momentum, and volatility features.
- Strict OHLCV validation: malformed, duplicate, stale, and still-open candles cannot produce trading signals.
- No-lookahead walk-forward calibration with Brier score, expectancy, drawdown, and directional win rates.
- A persistent signal ledger that settles issued ideas after the configured horizon and measures realised performance.
- A first-class
NO ACTIONstate when the directional edge is below the watch threshold. - HTML-formatted Telegram messages ready for public channels or private trading groups.
- Utility scripts to capture
/startsubscribers and maintain offsets without ever touching the BotFather dashboard again. - First-class automation: cron friendly and bundled with GitHub Actions workflows for both signal runs and subscriber syncing.
- Python 3.13
- pandas, numpy, requests, python-dotenv
- Telegram Bot API
- CryptoCompare OHLCV with public Binance Spot market-data fallback
# 1) Clone the repo
git clone https://github.com/ihoooman/Signal-Bot.git
cd xrp-signal-bot
# 2) Spin up a virtual environment
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# 3) Install dependencies
pip install -r requirements.txt
# 4) Prepare configuration
cp .env.example .envFill in .env with your secrets:
BOT_TOKEN=bot-token-from-botfather
# TELEGRAM_BOT_TOKEN=bot-token-from-botfather # optional legacy fallback
TELEGRAM_CHAT_ID=chat-or-channel-id (optional fallback)
CRYPTOCOMPARE_API_KEY=cryptocompare-api-key (optional)
# SUBSCRIBERS_DB_PATH=/absolute/path/to/subscribers.sqlite3 (optional override)
# SUMMARY_CRON=0,4,8,12,16,20 # 4-hour summary slots (Asia/Tehran hours)
# EMERGENCY_INTERVAL_H=2 # Emergency sweep cadence in hours
# BROADCAST_SLEEP_MS=0 # Delay between chat sends to respect rate limits
# DEFAULT_ASSETS=XRPUSDT,BTCUSDT,ETHUSDT # Seed watchlists when empty
# DONATION_TIERS=100,500,1000 # Telegram Stars donation buttons
# ADMIN_CHAT_IDS=123456789 # Comma-separated admin chat ids for /donations and /refund
# TIMEZONE=Asia/Tehran # Override the scheduler timezone if needed
CONF_MIN=0.70 # Minimum probability for actionable ideas
MIN_SIGNALS=8 # Emit at least this many symbols per summary
ALPHA_SIGMOID=2.0 # Logistic sharpness for probability mapping
BEAR_SELL_BOOST=1.15 # Multiplier when global context is risk-off
RISKON_BUY_BOOST=1.10 # Multiplier when global context is risk-on
WATCH_BAND_LOW=0.55 # Lower bound for watchlist classification
RESULT_HORIZON_HOURS=24 # Horizon used to settle issued signal outcomes
OPTIMIZE_HOUR=3 # Daily walk-forward calibration hour (TIMEZONE)
Actionable ideas require CONF_MIN (default 70%) and sit in the 🟢 section. Entries that clear WATCH_BAND_LOW but not CONF_MIN appear under 🟡 Watch. Anything below WATCH_BAND_LOW is explicitly reported as NO ACTION; it is never promoted into an emergency signal. The Experimental bucket can still backfill a summary to MIN_SIGNALS, but those entries remain non-actionable.
Every live data frame is sorted, deduplicated, checked for valid OHLC relationships, stripped of its open candle, and rejected when stale. Signal messages identify the actual market-data source and closed-candle timestamp. Once walk-forward calibration has completed, messages also include the historical calibration sample size.
Tips:
- Set
ENV_FILEin the environment to point at a custom config file if you deploy outside the repo root. SUBSCRIBERS_DB_PATHcontrols where the SQLite-backed subscriber registry lives whenDB_URLis not set. Set it to a persistent volume in production deployments. LegacySUBSCRIBERS_PATHvalues are treated as the same override for backwards compatibility.- When the database contains at least one active chat id, the bot will broadcast to all of them.
TELEGRAM_CHAT_IDis used only as a fallback or for smoke tests.
python send_test.pyA Status: 200 response means the bot token and chat id are valid and Telegram can reach your endpoint.
Capture /start messages, request phone numbers, and maintain your subscriber list:
python listen_start.pyBehind the scenes:
listen_start.pynow replies to/startwith a one-tap "📱 ارسال شماره من" button. Users become subscribed only after sharing their Telegram phone number, which is stored in the subscriber database (PostgreSQL whenDB_URLis set, otherwise SQLite) alongside their chat id.listen_updates.pyis a thin wrapper aroundlisten_start.pyfor backwards compatibility.data/offset.txtprevents duplicate processing and is updated after every batch so repeated Telegram fetches stay idempotent. Keep both files private; they contain user identifiers.- Override the storage path with
SUBSCRIBERS_DB_PATHwhen you want to place the database outside the repository (e.g., on a persistent volume). The schema enforces a unique Telegram user id and an index on phone numbers for quick lookups. - After a contact is registered the bot sends an inline menu with four bilingual buttons: "📬 دریافت فوری / Get updates now" replays the most recent cached BUY / SELL / NO ACTION snapshot when it is still fresh, otherwise it triggers a one-off live evaluation, refreshes
data/last_summary.json, and highlights any emergencies captured during that run. "➕ افزودن ارز / Add asset" lets the user extend their personal watchlist (default quoteUSDTunless they type another quote such asSOLUSDC), "🗑️ حذف ارز / Remove asset" removes pairs from the per-user watchlist with confirmation prompts and pagination when needed, and "💖 دونیت با استارز / Donate with Stars" opens the Telegram Stars drawer with preconfigured tiers (defaultDONATION_TIERS) plus a custom amount prompt. New subscribers that have not customised their watchlist yet are automatically seeded with the comma-separated symbols fromDEFAULT_ASSETSso/getand the inline shortcut always return something meaningful. - Each selection is stored in the subscriber database under
user_watchlist (user_id, symbol_pair, created_at)with uniqueness enforced per user, so the two-hour emergency sweep, four-hour summaries, and on-demand updates always include custom pairs. - Users can send
/menuat any time to re-open the bilingual inline keyboard,/getto receive the latest cached snapshot instantly,/donateto open the Stars tiers directly,/helpfor a quick list of commands, or/cancelto abandon the add-asset flow. - The donation drawer uses Telegram's native Stars invoices (
currency=XTR, no provider token)./termsprovides a short terms notice,/paysupportexplains how to reach payment support, and admins listed inADMIN_CHAT_IDScan review the last donations via/donationsor request a refund with/refund <telegram_payment_charge_id>.
Set DB_URL when you want to store subscribers and summary snapshots in PostgreSQL instead of the local SQLite file. The value should be a standard psycopg2-compatible connection string, for example:
DB_URL=postgresql://bot_user:superSecret@ep-iced-forest-123456.us-east-2.aws.neon.tech/neondb?sslmode=require
When the variable is present the bot automatically establishes a connection pool (with SSL if requested), creates the required tables (subscribers, summaries, user_watchlist, donations), and migrates any existing subscribers.json, subscribers.sqlite3, or data/last_summary.json content into PostgreSQL. If DB_URL is absent the bot falls back to a local SQLite file at SUBSCRIBERS_DB_PATH (or subscribers.sqlite3 next to the codebase).
To verify connectivity and trigger the migration manually run:
python migrate_db.pyThe CLI prints the resolved backend, ensures the schema exists, migrates legacy data, and reports the current subscriber count. You can pass --path /tmp/test.sqlite3 to probe an alternate SQLite file during local testing.
subscribers.is_subscribed and subscribers.awaiting_contact are stored as real PostgreSQL booleans. All write paths coerce values such as 0/1, "true"/"false", and None into proper True/False flags before binding parameters, and the CLI migration (python migrate_db.py) upgrades legacy integer columns via ALTER TABLE … USING … before inserting a throwaway record to verify the conversion.
If GitHub reports conflicts on files such as listen_start.py, listen_updates.py, subscriptions.py, or the associated tests,
it means new commits have touched the same sections of code since this branch was created. Rebase (or merge) the latest main
branch locally to bring those updates in, fix the conflicts, and push again:
git checkout work
git fetch origin
git rebase origin/main # or: git merge origin/main
# fix the conflicts shown by Git and stage the resolved files
git add listen_start.py listen_updates.py subscriptions.py tests/test_listen_updates.py tests/test_subscriptions.py
git rebase --continue # or: git commit
git push --force-with-leaseAfter the branch is updated without conflicts, GitHub will allow the pull request to merge cleanly.
- Tap "💖 دونیت با استارز / Donate with Stars" to pick a tier from
DONATION_TIERSor enter a custom value in Stars. - Payments are handled entirely inside Telegram via native invoices (digital goods). If the client cannot process Stars payments, the bot replies with a friendly fallback message.
- Successful payments are stored in the subscriber database
donationstable along with the Telegram charge id so/donationscan list the last 20 entries and totals. - Admins can issue
refundStarPaymentcalls by running/refund <telegram_payment_charge_id>from an approved chat id (comma-separated inADMIN_CHAT_IDS).
python trigger_xrp_bot.py
python trigger_xrp_bot.py --mode emergencyBehaviour:
- Aggregates validated, closed OHLCV candles from CryptoCompare or the Binance Spot fallback for each tracked symbol and quote currency.
- Rejects stale or malformed feeds instead of converting data failures into weak signals.
- Runs the ensemble, applies stored walk-forward calibration, and builds an HTML message block per asset.
- Groups outputs into actionable, watch, experimental/NO ACTION, and data-unavailable buckets.
- In
--mode emergency, the bot checks conditions every 2 hours and emits a "🚨 EMERGENCY SIGNAL" message only when BUY or SELL criteria are satisfied. - Issued directional signals are stored idempotently in
signal_events; due events are settled against the latest validated close during later runs.
Cron (self-hosted):
*/30 * * * * /path/to/venv/bin/python /path/to/repo/trigger_xrp_bot.py >> /var/log/xrpbot.log 2>&1GitHub Actions:
.github/workflows/emergency.ymlruns every two hours and performs a three-stage pipeline:prehandle,emergency, and a database-backedsnapshotrefresh..github/workflows/summary.ymlruns at Tehran fixed slots and follows the same pipeline for the scheduled summary..github/workflows/subscriber-listener.ymlpolls pending Telegram updates every five minutes and persists subscriber state in the configured database; it no longer commits runtime state into Git..github/workflows/optimize.ymlruns the no-lookahead walk-forward calibration once per day and persists model parameters in the database..github/workflows/ci.ymlruns Ruff, the test suite, and a dependency vulnerability audit on every pull request.- All production workflows share one concurrency group, preventing two Telegram pollers or broadcasters from running at the same time.
- Every workflow can also be invoked manually with
workflow_dispatchfor smoke testing.
Secrets to configure:
BOT_TOKEN(Telegram bot token)DB_URL(PostgreSQL connection string such aspostgresql://user:pass@host/db?sslmode=require; enables Neon or any external database)ADMIN_CHAT_IDS,DONATION_TIERS, andTIMEZONEoverride defaults if needed.- Optional:
TELEGRAM_CHAT_IDandCRYPTOCOMPARE_API_KEYremain supported for fallback broadcasts and higher API rate limits.
python -m src.signal_bot.ci_entry --mode <stage> is the single entry point each job calls, so you can reproduce the GitHub Actions behaviour locally (prehandle, emergency, summary, snapshot, optimize).
Worker mode: When you run a dedicated poller (for example a Render Worker) keep the process scale at 1 and disable the
prehandlestage in GitHub Actions. Only one consumer should callgetUpdatesat a time to avoid duplicate prompts.
run.py requires APScheduler and fails visibly if its production scheduler cannot start. The standalone schedule_jobs.py command still reports a clear warning when the optional scheduler import is unavailable.
The bundled run.py daemon uses one APScheduler instance for summary, emergency, and daily calibration jobs. Scheduler exceptions are logged instead of silently discarded, and shutdown is graceful.
Choose either the always-on
run.pydeployment or scheduled production workflows. Do not enable both against the same bot token.
trigger_xrp_bot.py— signal engine and Telegram broadcaster.src/signal_bot/data_quality.py— closed-candle, freshness, schema, and OHLC validation.src/signal_bot/regime_engine.py— ensemble features and no-lookahead walk-forward calibration.listen_start.py,listen_updates.py— helper scripts for capturing subscribers.send_test.py— quick health-check for bot credentials.subscribers.sqlite3,data/offset.txt— runtime data stores (excluded from git).requirements.txt,requirements-dev.txt— production and validation dependencies..github/workflows/— ready-to-use automation pipelines..env.example— starter template for configuration.
Deploying the long-polling bot on Render as a free Web Service keeps everything in one process:
- Build Command:
pip install -r requirements.txt - Start Command:
python run.py - Environment Variables:
BOT_TOKEN(required)DB_URL(PostgreSQL connection string for Neon or similar; omit to stay on SQLite)TIMEZONE=Asia/Tehran- Optional overrides:
ADMIN_CHAT_IDS,DONATION_TIERS
Render injects the PORT variable at runtime; health.py binds to it so Render detects the listening socket while listen_updates continues long-polling in parallel. When running without DB_URL, mount a persistent disk for the SQLite files (subscribers.sqlite3, data/offset.txt). When DB_URL points at Neon or another managed PostgreSQL instance the state survives deploys automatically. To minimise cold starts you can ping the health endpoint with a keep-alive service such as UptimeRobot.
- Never commit
.env,subscribers.json, ordata/offset.txt. They are already ignored in.gitignore. - Rotate your Telegram bot token if it ever leaks.
- Use a dedicated CryptoCompare key so you can monitor usage and revoke access without downtime.
- When running on shared infrastructure, set
SUBSCRIBERS_DB_PATHto a protected directory with restricted permissions. - Use
DB_URLin GitHub Actions or other ephemeral environments. Without persistent storage, subscriber, model-calibration, and signal-performance state will not survive the job.
Security note: If the bot token ever leaks, immediately rotate it via @BotFather and update the
BOT_TOKENenvironment variable on Render (or any other deployment target) before re-deploying.
Interested in sharpening the signal logic, adding tickers, or wiring up alternative data providers? Open an issue or submit a pull request. Before sending a patch run:
ruff check .
pytest -q
pip-audit -r requirements.txtHappy trading!