Most "run a trading bot" tutorials stop at backtest passed, bot started. That is the easy 20%. The hard 80% is what happens on day 4 at 03:00 when the process is gone and you only notice three weeks later — with a flat equity curve and a dead clock.
This post is the part nobody writes: a production-shaped, zero-cost setup where the bot restarts itself, logs its own P&L, and hard-stops on a risk rule you define. Everything below was built and verified in a single afternoon on a $0 budget, using only open-source tooling. The dry-run trades are simulated — no exchange key, no real money, no risk.
Why a watchdog matters more than the strategy
A strategy is a hypothesis. A watchdog is what turns a hypothesis into data.
If your bot silently dies, every metric you were collecting stops: win rate, drawdown, profit factor. You think you are "running a 14-day validation" while in reality the last trade was nine days ago. A watchdog fixes three things cheaply:
- Availability — the process is restarted automatically.
- Observability — a P&L snapshot is appended to a log on every cycle, so you have a time series instead of a vibe.
- Risk — a global rule (e.g. stop if drawdown ≥ 10% of the wallet) is enforced by code, not by willpower.
Prerequisites
- Linux (or WSL2) with ~3 GB RAM.
-
uvfor the Python environment. On modern distrospip installinto the system Python is blocked by PEP 668, anduvsidesteps that cleanly.
curl -LsSf https://astral.sh/uv/install.sh | sh
uv venv ~/freqtrade/.venv
source ~/freqtrade/.venv/bin/activate
uv pip install freqtrade
A minimal, sane config
Start with a cosmetic config and tune later. The important bits for a safe dry-run:
{
"max_open_trades": 3,
"stake_currency": "USDT",
"stake_amount": 50,
"dry_run": true,
"dry_run_wallet": 500,
"trading_mode": "spot",
"exchange": {
"name": "binance",
"key": "",
"secret": "",
"pair_whitelist": ["BTC/USDT", "ETH/USDT", "SOL/USDT"]
},
"pairlists": [{ "method": "StaticPairList" }],
"api_server": {
"enabled": true,
"listen_ip_address": "127.0.0.1",
"listen_port": 8080,
"username": "hermes",
"password": "CHANGE_ME",
"jwt_secret_key": "CHANGE_ME_TOO",
"ws_token": "CHANGE_ME_AS_WELL"
},
"bot_name": "dryrun",
"initial_state": "running"
}
Two rules that will save you hours:
-
Bind the API to
127.0.0.1, never0.0.0.0. The API exposes order and config operations. -
Set
jwt_secret_key,ws_tokenand a real password even in dry-run. Empty secrets are a production foot-gun waiting to be copied into live.
Backtest before you believe anything
Download history and backtest several strategies in their native timeframe. A common mistake is backtesting a 5-minute scalp strategy on 1-hour candles and concluding it is bad — you are measuring the timeframe, not the strategy.
freqtrade download-data --config config.json --days 200 --timeframes 5m 15m 1h
for s in SwingHighToSky BbandRsi UniversalMACD; do
freqtrade backtesting --config config.json --strategy "$s" \
--timerange 20260310-20260925 --breakdown
done
A real result set from a 200-day window (fees included) looked like this:
| strategy | timeframe | trades | win% | profit | profit factor | max drawdown |
|---|---|---|---|---|---|---|
| SwingHighToSky | 15m | 108 | 68.5% | +1.60% | 5.21 | 0.37% |
| BbandRsi | 1h | 62 | 74.2% | +3.99% | 1.38 | 7.92% |
| UniversalMACD | 5m | 6 | 100% | +1.48% | ∞ | 0.00% |
Read that table honestly. UniversalMACD shows a perfect win rate — on six trades. That is not an edge, that is a sample size. BbandRsi has the highest absolute return but a weak profit factor. SwingHighToSky is the only one passing all three filters (PF > 1.5, drawdown < 15%, win rate > 55%) on a statistically usable sample. Pick on the quality of the evidence, not on the biggest number.
Also note: none of them beat buy-and-hold over a window where the market rose ~31%. The dry-run's job is to validate operational behaviour, not to beat the market. Be suspicious of any backtest that does.
The watchdog (the actually interesting part)
Here is a working watchdog, designed to run from cron and stay silent unless something is wrong.
#!/bin/bash
# - restarts the bot if the process died
# - appends a P&L snapshot to logs/daily_pnl.txt on every run
# - SILENT on success; speaks only on restart or risk-limit breach
set -u
BASE=/home/kali/freqtrade
VENV_PY=$(readlink -f "$BASE/.venv/bin/python")
cd "$BASE" || { echo "watchdog: cannot cd $BASE"; exit 1; }
PY=$BASE/.venv/bin/python
LOG=$BASE/logs/watchdog.log
ts() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
# Print PIDs of the REAL bot processes (empty if none).
find_bot() {
for p in /proc/[0-9]*; do
pid=${p#/proc/}
[ -r "$p/cmdline" ] || continue
cmd=$(tr '\0' ' ' < "$p/cmdline" 2>/dev/null) || continue
case "$cmd" in
*"freqtrade trade --config"*)
exe=$(readlink -f "$p/exe" 2>/dev/null)
[ "$exe" = "$VENV_PY" ] && echo "$pid"
;;
esac
done
}
MSG=""
if [ -z "$(find_bot)" ]; then
echo "$(ts) bot not running -> restarting" >> "$LOG"
nohup "$BASE/.venv/bin/freqtrade" trade --config "$BASE/config.json" \
--strategy SwingHighToSky --dry-run >> "$BASE/logs/trade_stdout.log" 2>&1 &
sleep 30
if [ -n "$(find_bot)" ]; then
MSG="crypto-bot: process was gone, restarted at $(ts)."
else
echo "$(ts) restart FAILED" >> "$LOG"
exit 1
fi
fi
OUT=$("$PY" "$BASE/monitor_pnl.py" 2>&1); RC=$?
if [ $RC -eq 2 ]; then # drawdown limit hit
for pid in $(find_bot); do kill "$pid" 2>/dev/null; done
echo "$(ts) DRAWDOWN LIMIT HIT -> stopping bot" >> "$LOG"
echo "crypto-bot: drawdown >= 10% of wallet. Bot STOPPED by risk rule."
echo "$OUT"
exit 2
fi
echo "$OUT" >> "$LOG"
[ -n "$MSG" ] && echo "$MSG"
exit 0
The gotcha that cost me an hour
Do not detect the process with pkill -f freqtrade or pgrep -f freqtrade. That pattern also matches the shell that invoked your script — because the string freqtrade appears in its command line — and the script kills itself. I hit exactly this. Scanning /proc and requiring both the freqtrade trade --config cmdline and an exe pointing into the venv is unambiguous and safe.
Make it run without babysitting
# every 15 minutes; quiet unless it acts
*/15 * * * * /home/kali/.hermes/scripts/crypto_watchdog.sh
Verify it end-to-end the first time: kill the bot, run the watchdog, confirm the restart and the P&L line. A watchdog you have never seen fire is a watchdog you do not have.
The kill switch is the whole point
The drawdown rule (>= 10% of the wallet → stop the bot) is enforced outside the strategy, in the watchdog. Strategy-level stoplosses protect a trade; this protects the account. Keeping it in one place — not scattered across strategy parameters — means you can reason about your worst case at a glance.
Results after the first hours
wallet=500.10 closed_pnl=+0.000USDT (+0.00%) all_pnl=+0.003USDT (+0.00%) trades=0W/0L open=1
Zero closed trades, one open position, equity essentially flat. That is the correct shape for hour one of a dry-run. The value is not the P&L yet — it is that the clock is now running on a validated, self-healing, risk-capped system, at zero cost.
What to do next
- Let it run for two weeks. Judge the behaviour: did the watchdog ever fire? did the kill switch ever trip? how did it behave around a volatility spike?
- Hyperopt deliberately — a handful of epochs with a Sharpe-based loss, then re-backtest. Do not hyperopt your way into a curve fit.
- Only then consider real capital — and only with an amount you are genuinely fine losing.
The whole setup costs nothing and removes the two failure modes that kill most hobby bots: a dead process and an unenforced risk rule.
If you build on this, the two files worth stealing are the /proc-based process check and the drawdown kill switch. Everything else is configuration.
Want this pre-built? I packaged the watchdog, the P&L logger, the backtest runner and the config
into a drop-in kit — Freqtrade Self-Healing Starter Kit ($9).
It is the exact code from this article plus a setup guide with the /proc gotcha already solved,
so you can go from zero to a self-healing bot in about five minutes.
Related in this series:
- Keep any long-running Python process alive on Linux for $0 (the /proc watchdog pattern)
- I backtested 14 trading strategies. Only one passed my filters.
- Keep Freqtrade running 24/7: systemd vs a zero-dependency watchdog
The watchdog from this post is open source: **https://github.com/moon-hacks/proc-watchdog* (MIT).*
Prefer someone to just do it? I also offer a **done-for-you setup* (https://tntofficial.gumroad.com/l/vweza) — Freqtrade installed, backtested and left running in dry-run with the watchdog active.*
Top comments (0)