TradeX is an advanced, web-based trading bot designed to automate cryptocurrency trading across major exchanges like Binance and Bybit. It integrates seamlessly with TradingView webhooks and email alerts, enabling real-time trade execution based on custom signals. The system provides a secure and user-friendly dashboard for managing open positions, pending orders, and executing trades.
Key features include:
- Multi-exchange support (Bybit, Binance).
- Secure authentication and PIN protection for trade execution.
- Real-time monitoring of open positions and pending orders.
- Support for both webhook and email-based signal ingestion.
- Containerized deployment using Docker.
- Features
- Screenshots
- Supported Exchanges
- Supported Modes
- Installation
- Configuration
- Usage
- Running TradeX as a Service
- Running TradeX in Docker recommended
- Using Nginx as a Proxy for Webhooks
- Troubleshooting
- Security Best Practices
- Known Issues
- License
- Support the Project
- Multi-Exchange Support: Seamlessly integrates with Bybit and Binance Futures.
- Order Management: Cancel positions and orders directly from the dashboard.
- Real-Time Monitoring: View all active and pending orders in a user-friendly interface.
- Close All Positions: Quickly close all open positions with a single click.
- Signal Ingestion: Supports both webhook and email-based trade signals.
- Authentication: Secure dashboard with password protection.
- PIN Protection: Add an extra layer of security by requiring a PIN for order execution via webhooks or emails.
- Logging: Comprehensive logging for debugging and monitoring.
- Bybit Futures
- Binance Futures (not fully tested)
- Additional exchanges can be added upon request or contributed via pull requests
TradeX offers two distinct modes for receiving trade signals: Webhook Mode and Email Mode. These modes provide flexibility depending on your infrastructure, preferences, and technical setup. You can configure the mode using the MODE environment variable in the .env file (MODE=webhook, MODE=email, or MODE=both).
Webhook Mode allows TradeX to listen for real-time trade signals sent via HTTP POST requests. This mode is ideal for users with stable internet connections, a public IP address, and access to domain hosting.
- Real-Time Execution: Signals are processed instantly as soon as they are received.
- Low Latency: Minimal delay between signal generation (e.g., from TradingView) and order execution.
- Secure Authentication: Requires a configurable PIN (
WEBHOOK_PIN) to ensure only authorized signals are processed. - Integration with TradingView: Easily integrates with TradingView alerts using webhooks.
- A public IP address or domain name pointing to your server.
- A stable internet connection to ensure uninterrupted communication.
- Ports
80or443must be open and accessible (or proxied via Nginx/Apache). - Optional: SSL/TLS certificate for secure HTTPS communication.
If you have a dedicated server or VPS with a public IP and want the fastest possible execution of trades, Webhook Mode is the best choice. It’s particularly suited for advanced traders who rely on real-time market data and fast execution.
It’s worth noting that TradingView webhooks are not guaranteed to be delivered without delays or failures. While TradingView provides webhook functionality for sending alerts, there are inherent limitations:
- Delivery Delays: Webhooks may experience delays due to high volatility.
- Response Time Requirements: TradingView expects a quick response (typically within milliseconds). If your application takes too long to respond, TradingView may retry the request or consider it failed.
- No Delivery Guarantees: There are numerous reports online of missed or failed webhook deliveries.
To mitigate these issues, consider using Email Mode as a fallback or running TradeX in Dual Mode (MODE=both) for redundancy.
Email Mode allows TradeX to process trade signals sent via email. This mode is perfect for users who do not have a public IP, stable internet, or access to domain hosting. Instead of relying on HTTP requests, TradeX monitors an email inbox for unread emails containing trade signals.
- No Public IP Required: Works entirely through email, so there’s no need for port forwarding or domain hosting.
- Offline-Friendly: Even if your internet connection drops temporarily, emails will be queued by the email provider and processed once the connection is restored.
- Simple Setup: Just configure your email credentials (IMAP) and send trade signals via email.
- Flexible Signal Format: Trade signals can be embedded in the email subject line as JSON.
- An email account with IMAP access enabled.
- Properly formatted trade signals in the email subject line (JSON format).
- No need for a public IP, domain, or open ports.
If you don’t have access to a public IP or stable internet, Email Mode is the ideal solution. It’s also a great fallback option for users who want redundancy in case their webhook setup fails.
For maximum flexibility, TradeX supports running in Dual Mode (MODE=both). In this mode, TradeX listens for signals from both webhooks and emails simultaneously. This ensures you never miss a trade signal, regardless of your connectivity or infrastructure.
- Versatility: Combine the speed of webhooks with the reliability of email-based signals.
- Customizable Workflow: Use webhooks for high-priority, real-time signals and emails for less time-sensitive trades.
A trader uses TradingView webhooks for real-time signals during active trading hours but switches to email alerts for overnight or low-priority trades. By enabling both modes, they ensure continuous operation without manual intervention.
When running in Dual Mode, it’s important to configure your TradingView alerts carefully to avoid duplicate signal processing:
-
Tick "Send Email" Only: Use this for signals that don't require instant execution but need guaranteed delivery.
-
Tick "Webhook URL" Only: Use this for signals that require fast execution.
-
Ticking Both Boxes: the same signal is delivered twice, once by webhook and once by email. TradeX suppresses the second one — the two services share a duplicate store on disk, so whichever path arrives first places the order and the other is ignored. See Duplicate signals.
Suppression is bounded by WEBHOOK_DEDUP_SECONDS (60 by default), so it
covers near-simultaneous delivery down both paths, not an email that arrives
an hour late. Splitting signals by channel remains a reasonable approach:
- High-priority signals (e.g., scalping strategies) can be sent via webhook for fast execution.
- Lower-priority signals (e.g., long-term position adjustments) can be sent via email for guaranteed delivery.
Set the MODE variable in your .env file to one of the following options:
MODE=webhook: Only listen for webhook signals.MODE=email: Only process email signals.MODE=both: Listen for both webhook and email signals simultaneously.
- Security: Both modes support PIN protection (
WEBHOOK_PIN) to prevent unauthorized signal processing. - Testing: You can test both modes independently to ensure they work as expected before deploying in production.
- Python 3.10 or later (every dependency requires
>=3.10; the Docker image uses 3.14) pippackage manager- API keys for supported exchanges (e.g., Bybit, Binance)
- Docker (optional, for containerized deployment)
-
Clone the Repository:
git clone https://github.com/Sentello/tradex.git cd tradex -
Set Up a Virtual Environment (optional but recommended):
python3 -m venv venv source venv/bin/activate -
Install Dependencies:
pip install -r requirements.txt
-
Configure Environment Variables:
- Rename
.env.exampleto.envor create a new.envfile. - Add your exchange API keys, dashboard password, webhook PIN, and other required configurations.
Alternatively, you can use the
generate_credentials.pyscript to generate secure credential values:python generate_credentials.py
This will generate
FLASK_SECRET_KEY,WEBHOOK_PIN, and a bcryptDASHBOARD_PASSWORDhash to copy into your.envfile. Paste the printed hash, not the password you typed. - Rename
-
Run the Application Locally:
python main.py
-
Access the Dashboard:
- Open your browser and go to
http://localhost:5000.
- Open your browser and go to
The test suite covers signal validation, webhook authentication, position
closing and the dashboard login. pytest is not a runtime dependency, so
install it separately:
pip install pytest
pytestTo check the dependencies for known vulnerabilities:
pip install pip-audit
pip-audit -r requirements.txtTradeX has two separate secrets, and mixing them up is the most common setup mistake:
| Credential | Authenticates | Where you enter it |
|---|---|---|
WEBHOOK_PIN |
Incoming trade signals | Inside the JSON body of your TradingView alert (and email alerts) |
DASHBOARD_PASSWORD |
You, in a browser | The dashboard login form |
The webhook PIN will not log you into the dashboard. Use different values for the two: the PIN travels in plaintext inside alert bodies, so it should never also unlock the control panel.
The app refuses to start if any of these are missing, rather than falling
back to an insecure default. Generate all three with
python generate_credentials.py.
| Variable | Description |
|---|---|
FLASK_SECRET_KEY |
Signs dashboard session cookies. A predictable value lets anyone forge a login. |
DASHBOARD_PASSWORD |
bcrypt hash of the dashboard password, not the password itself. |
WEBHOOK_PIN |
Authenticates trade signals. This is the only credential on /webhook, so make it long and random. |
| Variable | Default | Description |
|---|---|---|
BYBIT_API_KEY |
— | API key for Bybit. |
BYBIT_API_SECRET |
— | API secret for Bybit. |
BINANCE_API_KEY |
— | API key for Binance. |
BINANCE_API_SECRET |
— | API secret for Binance. |
EXCHANGES |
bybit,binance |
Which exchanges to enable. An exchange is only loaded if it is listed here and has both a key and a secret. |
| Variable | Default | Description |
|---|---|---|
MODE |
webhook |
webhook, email, or both. In webhook mode the email reader exits cleanly; in email mode /webhook returns 503. |
| Variable | Default | Description |
|---|---|---|
IMAP_SERVER |
— | IMAP server address (e.g. imap.gmail.com). |
IMAP_PORT |
993 |
IMAP server port. |
IMAP_EMAIL |
— | Mailbox receiving trade signals. |
IMAP_PASSWORD |
— | Password for that mailbox. |
IMAP_USE_SSL |
true |
Use implicit SSL. When false, STARTTLS is used instead. |
IMAP_CHECK_INTERVAL |
15 |
Seconds between inbox checks. |
| Variable | Default | Description |
|---|---|---|
WEBHOOK_ALLOWED_IPS |
(empty — any address) | Comma-separated IPs or CIDR blocks allowed to reach /webhook. Off by default. See Restricting webhook sources. |
WEBHOOK_MAX_FAILURES |
5 |
Bad PINs from one address before it is locked out. |
WEBHOOK_LOCKOUT_SECONDS |
300 |
How long that lockout lasts. Applied per source IP, never endpoint-wide, so an attacker cannot stop your real alerts by sending junk. |
WEBHOOK_DEDUP_SECONDS |
60 |
Ignore an identical signal repeated within this window, so a lost response cannot become a doubled position. See Duplicate signals. Set to 0 to disable. |
TRUST_PROXY_HEADERS |
false |
Read the client address from X-Forwarded-For. Only enable behind a reverse proxy that overwrites the header — if the app is directly exposed, a client can forge it and bypass both the allowlist and the lockout. |
WEBHOOK_ALLOWED_IPS limits /webhook to a set of addresses. It is off
by default so a fresh clone works immediately — enabling it is your
decision, and you own the consequences either way.
TradingView publishes the source IPs it sends webhooks from:
52.89.214.238
34.212.75.30
54.218.53.128
52.32.178.7
To allow only those, uncomment this line in .env and restart:
WEBHOOK_ALLOWED_IPS=52.89.214.238,34.212.75.30,54.218.53.128,52.32.178.7CIDR blocks work too (WEBHOOK_ALLOWED_IPS=10.0.0.0/24,203.0.113.7), which
is the better fit when signals reach you through a proxy or VPS you control.
Understand the trade-off before turning it on. With an allowlist,
guessing the PIN stops being a viable attack at all. But TradingView does
not contractually guarantee those addresses — if they change and your list
is stale, real trade signals are rejected with a 403 and you will not
notice until you read logs/webhook.log. A missed entry can cost more than
the risk it removes. Verify the list against the link above before enabling,
and check it again if signals stop arriving.
Leaving it off is a reasonable choice. The per-IP lockout
(WEBHOOK_MAX_FAILURES) still applies either way, and a long random
WEBHOOK_PIN is the control that works regardless of where signals come
from.
If the connection drops after the exchange accepted your order but before
the reply gets back, the sender cannot tell that apart from a genuine
failure — and resending would double your position. TradeX therefore
ignores an identical signal repeated within WEBHOOK_DEDUP_SECONDS
(60 by default), answering 200 with:
{"status": "duplicate", "message": "Identical signal already accepted; no order placed."}The store is a small SQLite file next to the logs, shared by the webhook and
the email reader. That is what makes MODE=both safe: the two run as
separate processes, and an in-memory cache could not see what the other had
already executed. It also survives a restart, so a crash cannot replay a
signal.
Two signals count as identical when every field except PIN matches. If
your strategy can legitimately fire the same order twice in quick
succession, add a unique ID field to the alert — when ID is present it
alone decides identity, so genuine repeats always execute:
{"PIN": "your_pin", "ID": "{{timenow}}", "EXCHANGE": "bybit", "SYMBOL": "BTC/USDT:USDT",
"SIDE": "buy", "ORDER_TYPE": "market", "QUANTITY": "0.01"}A locally rejected signal (400) is not remembered, since nothing reached
the exchange and a corrected retry must go through. A signal the exchange
rejected (502) is remembered, because there is no way to prove the order
did not land.
This is at-most-once delivery within a window, not protection against a
determined attacker — a captured request can still be replayed after the
window expires. WEBHOOK_PIN and WEBHOOK_ALLOWED_IPS are the controls
for that.
| Variable | Default | Description |
|---|---|---|
SESSION_COOKIE_SECURE |
false |
Set to true when serving the dashboard over HTTPS, so the session cookie is never sent in cleartext. |
SESSION_LIFETIME_HOURS |
12 |
How long a dashboard login stays valid. |
LOGIN_MAX_ATTEMPTS |
5 |
Failed logins from one address before lockout. |
LOGIN_LOCKOUT_SECONDS |
300 |
How long that lockout lasts. |
| Variable | Default | Description |
|---|---|---|
DASHBOARD_HOST |
0.0.0.0 |
Dashboard bind address. Set 127.0.0.1 for a local (non-Docker) run to keep it off the network. |
DASHBOARD_PORT |
5000 |
Dashboard port. |
WEBHOOK_HOST |
0.0.0.0 |
Webhook bind address. |
WEBHOOK_PORT |
5005 |
Webhook port. |
These are read by serve.py, which both main.py and supervisord.conf
use to start the services — so changing them takes effect without editing
any service file.
Under Docker these control the bind address inside the container,
which must stay 0.0.0.0 for port publishing to work. What reaches your
network is decided by the ports: list in docker-compose.yml, where the
dashboard is published to 127.0.0.1 only:
ports:
- "127.0.0.1:5000:5000" # dashboard: localhost only
- "5005:5005" # webhook: must be reachable by TradingViewThe dashboard serves no TLS and can close every open position, so it is kept
off the public network by default. Reach it over an SSH tunnel
(ssh -L 5000:127.0.0.1:5000 you@server), or put a TLS-terminating reverse
proxy in front and set SESSION_COOKIE_SECURE=true.
The webhook listener runs on port 5005. Use the following examples to test placing orders via webhooks:
curl -X POST http://localhost:5005/webhook \
-H "Content-Type: application/json" \
-d '{
"PIN": "123456",
"EXCHANGE": "bybit",
"SYMBOL": "BTCUSDT",
"SIDE": "buy",
"ORDER_TYPE": "market",
"QUANTITY": 0.01
}'curl -X POST http://<server-ip>:5005/webhook -H "Content-Type: application/json" -d '{
"PIN": "123456",
"EXCHANGE": "bybit",
"SYMBOL": "BTCUSDT",
"SIDE": "sell",
"ORDER_TYPE": "market",
"QUANTITY": 0.001
}'curl -X POST http://localhost:5005/webhook \
-H "Content-Type: application/json" \
-d '{
"PIN": "123456",
"EXCHANGE": "binance",
"SYMBOL": "ETHUSDT",
"SIDE": "sell",
"ORDER_TYPE": "limit",
"QUANTITY": 0.5,
"PRICE": 2000.50
}'curl -X POST http://<server-ip>:5005/webhook -H "Content-Type: application/json" -d '{
"PIN": "123456",
"EXCHANGE": "bybit",
"SYMBOL": "BTCUSDT",
"SIDE": "buy",
"ORDER_TYPE": "limit",
"QUANTITY": 0.05,
"PRICE": 91000
}'When integrating with TradingView, ensure placeholders are properly quoted to avoid JSON parsing errors. Example:
{
"PIN": "123456",
"EXCHANGE": "bybit",
"SYMBOL": "BTCUSDT",
"SIDE": "{{strategy.order.action}}",
"ORDER_TYPE": "market",
"QUANTITY": "{{strategy.order.contracts}}"
}Note: TradingView requires webhooks to use ports 80 or 443. Use Nginx as a reverse proxy to forward requests to port 5005. I strongly recommend using port 80 to avoid potential SSL-related complications and suggest using Nginx as a proxy for this setup.
You can run TradeX as a service using either Supervisor or systemd. See the HOWTO.md file for details.
-
Build and start the application:
docker-compose build --no-cache docker-compose up --build -d
or use
build_and_run.sh. -
Check the status of the containers:
docker ps | grep tradex -
View logs:
docker-compose logs -f
-
Stop the application:
docker-compose down
You need to expose the webhook listener on port 80 or 443, use Nginx as a reverse proxy:
-
Install Nginx:
sudo apt update sudo apt install nginx
-
Create a new configuration file:
sudo nano /etc/nginx/sites-available/tradex-webhook
-
Add the following content:
server { listen 80; server_name your.domain.com; location /webhook { proxy_pass http://127.0.0.1:5005; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } }
-
Enable the configuration and restart Nginx:
sudo ln -s /etc/nginx/sites-available/tradex-webhook /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl restart nginx
-
Update Your Webhook URL: When sending webhooks (from TradingView), use the URL pointing to your domain or server IP on port 80:
http://your.domain.com/webhook
- The app refuses to start: the error names the missing variable.
FLASK_SECRET_KEY,WEBHOOK_PINandDASHBOARD_PASSWORDare required, andDASHBOARD_PASSWORDmust be a bcrypt hash — runpython generate_credentials.py. - Cannot log into the dashboard: check you are not entering the
WEBHOOK_PIN. It is a separate credential and will never work on the login form — see Two different credentials. If you have lost the password, generate a new hash and replaceDASHBOARD_PASSWORDin.env. - Login returns "Too many failed attempts": you have hit
LOGIN_MAX_ATTEMPTSfrom this address. WaitLOGIN_LOCKOUT_SECONDS(5 minutes by default), or restart the dashboard to clear the counter, which is held in memory. - Error: "No exchanges loaded!": ensure your API keys are set in
.envand that the exchange is listed inEXCHANGES. - Webhook returns 403: the
PINin the alert body does not matchWEBHOOK_PIN, or the sender's address is not inWEBHOOK_ALLOWED_IPS. - Webhook returns 429: that source IP is locked out after repeated bad PINs. See
WEBHOOK_LOCKOUT_SECONDS. - Webhook returns 503:
MODEdoes not includewebhook. - Webhook answers
"status": "duplicate"and no order is placed: an identical signal arrived withinWEBHOOK_DEDUP_SECONDS. If that repeat was intentional, add a uniqueIDfield to the alert — see Duplicate signals. - Email Reader Issues: check IMAP credentials and ensure the email account allows IMAP access.
- Dashboard Not Accessible: ensure the Flask app is running and the correct port (
5000) is exposed.
For further assistance, check the logs in the logs/ directory. Each service writes its own file (webhook.log, dashboard.log, email_reader.log), also viewable from the dashboard.
- Consider restricting webhook sources:
WEBHOOK_ALLOWED_IPSremoves brute-forcing the PIN as a possibility rather than merely slowing it down. Off by default, with the trade-off explained in Restricting webhook sources — a stale address list silently rejects real signals, so it is your call whether the exchange is worth it. - Use a long, random
WEBHOOK_PIN: it is the only credential on an endpoint that places real orders. A 6-digit numeric PIN is a keyspace of 1,000,000 — crackable in about an hour at 100 requests/second.python generate_credentials.pyproduces a 43-character one. - Never reuse the PIN as the dashboard password: the PIN travels in plaintext inside alert bodies and across mail servers.
- Restrict access to the dashboard: bind it to
127.0.0.1or use a firewall. It has no TLS of its own, so the password and session cookie cross the network in cleartext unless you put it behind a reverse proxy. - Serve the dashboard over HTTPS and set
SESSION_COOKIE_SECURE=trueonce you do. - Regularly rotate API keys: periodically update your exchange API keys to minimize risks.
- Restrict exchange API key permissions: enable trading, but disable withdrawals and restrict the key to your server's IP where the exchange supports it.
- Never commit
.env: it is gitignored and excluded from the Docker image. Secrets are supplied at runtime viaenv_file. - Audit dependencies periodically:
pip-audit -r requirements.txt.
- Binance Futures: Support for Binance Futures is not fully tested.
- Email Parsing: The email reader assumes trade signals are always in the subject line. This may fail if the format changes in the future.
- Rate Limits: High-frequency trading may trigger rate limits on exchanges.
This project is licensed under the MIT License. See the LICENSE file for details.
If you find this project useful and would like to support me, consider making a donation.
bc1qm4zv6fwxuf8n5sdkrfc6ylxyhs6vhmkkvxcjf0
0xaa0ab64b0cdecb527eb5e7d5fc9ed94044c37a4c
Thank You!




