<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: matteo adorni</title>
    <description>The latest articles on DEV Community by matteo adorni (@matteo_adorni_948bc601a06).</description>
    <link>https://dev.to/matteo_adorni_948bc601a06</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4142991%2F013b919e-e7f3-43d0-a4f4-52300c70eb70.jpg</url>
      <title>DEV Community: matteo adorni</title>
      <link>https://dev.to/matteo_adorni_948bc601a06</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/matteo_adorni_948bc601a06"/>
    <language>en</language>
    <item>
      <title>Keep Freqtrade running 24/7: systemd vs a zero-dependency watchdog</title>
      <dc:creator>matteo adorni</dc:creator>
      <pubDate>Fri, 25 Sep 2026 15:22:31 +0000</pubDate>
      <link>https://dev.to/matteo_adorni_948bc601a06/keep-freqtrade-running-247-systemd-vs-a-zero-dependency-watchdog-2d44</link>
      <guid>https://dev.to/matteo_adorni_948bc601a06/keep-freqtrade-running-247-systemd-vs-a-zero-dependency-watchdog-2d44</guid>
      <description>&lt;p&gt;You built the strategy. You backtested it. You started it with &lt;code&gt;nohup&lt;/code&gt;. Three days later you check the P&amp;amp;L and… the process is gone. It has been gone since roughly two hours after you closed the terminal.&lt;/p&gt;

&lt;p&gt;"Keep Freqtrade running" is the number one operational question in every Freqtrade forum, and the answers split into two camps: &lt;strong&gt;systemd&lt;/strong&gt; and &lt;strong&gt;a cron watchdog&lt;/strong&gt;. I have run both in production-style setups. Here is the honest comparison, and the pattern I actually use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 1: systemd (the "proper" way)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/freqtrade.service
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Freqtrade&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;freqtrade&lt;/span&gt;
&lt;span class="py"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/home/freqtrade&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/home/freqtrade/.venv/bin/freqtrade trade --config user_data/config.json&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;span class="py"&gt;RestartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;30&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; freqtrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What you get:&lt;/strong&gt; automatic restart on crash, restart on reboot, &lt;code&gt;journalctl&lt;/code&gt; logs, clean &lt;code&gt;systemctl stop&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The catches, in practice:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It only restarts &lt;strong&gt;the process&lt;/strong&gt;. It does not check that the &lt;em&gt;thing is healthy&lt;/em&gt;. A Freqtrade process can be alive and stuck: exchange connection dropped, a hung API, a wedged websocket. &lt;code&gt;Restart=always&lt;/code&gt; sees a running PID and does nothing.&lt;/li&gt;
&lt;li&gt;No P&amp;amp;L record. You find out about a silent stall by noticing the trade count never moved.&lt;/li&gt;
&lt;li&gt;No risk rule. If your bot is bleeding, systemd faithfully keeps it bleeding.&lt;/li&gt;
&lt;li&gt;It needs root to install, and &lt;code&gt;sudo&lt;/code&gt; on a cheap VPS is a thing you have to think about.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;systemd is excellent at "make sure a binary is running". It is not an observability or risk layer, and it never claimed to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 2: cron + watchdog (the pattern I use)
&lt;/h2&gt;

&lt;p&gt;A tiny script, run every 15 minutes, that does three jobs: restart if dead, record health, and enforce a kill switch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;*&lt;/span&gt;/15 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /home/user/freqtrade/scripts/crypto_watchdog.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The restart detection is the part that matters, and the naive version is a trap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# DO NOT DO THIS&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; pgrep &lt;span class="nt"&gt;-f&lt;/span&gt; freqtrade &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;restart&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pgrep -f&lt;/code&gt; matches the &lt;strong&gt;full command line&lt;/strong&gt;, and your watchdog's own invocation may contain the pattern — so the script can match itself, conclude "all good", or &lt;code&gt;pkill&lt;/code&gt; its own shell. I hit exactly this and wasted an hour on a watchdog that was killing itself.&lt;/p&gt;

&lt;p&gt;Do it properly by scanning &lt;code&gt;/proc&lt;/code&gt; and requiring two independent facts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;VENV_PY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;readlink&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;/.venv/bin/python"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

find_bot&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;p &lt;span class="k"&gt;in&lt;/span&gt; /proc/[0-9]&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nv"&gt;pid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="p"&gt;#/proc/&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="s2"&gt;/cmdline"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;continue
    &lt;/span&gt;&lt;span class="nv"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'\0'&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &amp;lt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="s2"&gt;/cmdline"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;continue
    case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cmd&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
      &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="s2"&gt;"freqtrade trade --config"&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nv"&gt;exe&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;readlink&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="s2"&gt;/exe"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$exe&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VENV_PY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;esac&lt;/span&gt;
  &lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/proc/&amp;lt;pid&amp;gt;/cmdline&lt;/code&gt; is the real argv (NUL-separated), not a shell's mangled string.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/proc/&amp;lt;pid&amp;gt;/exe&lt;/code&gt; must resolve to &lt;strong&gt;your&lt;/strong&gt; venv interpreter — so you can never match a stray system Python or the watchdog itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then the loop adds the two things systemd does not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find_bot&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;nohup&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;/.venv/bin/freqtrade"&lt;/span&gt; trade &lt;span class="nt"&gt;--config&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;/config.json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--strategy&lt;/span&gt; SwingHighToSky &lt;span class="nt"&gt;--dry-run&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;/logs/trade_stdout.log"&lt;/span&gt; 2&amp;gt;&amp;amp;1 &amp;amp;
  &lt;span class="nb"&gt;sleep &lt;/span&gt;30
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find_bot&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ts&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; restart FAILED"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"crypto-bot: restarted at &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ts&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;   &lt;span class="c"&gt;# speaks only when it acts&lt;/span&gt;
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nv"&gt;OUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;/monitor_pnl.py"&lt;/span&gt; 2&amp;gt;&amp;amp;1&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;RC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$RC&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 2 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;                       &lt;span class="c"&gt;# health script says: drawdown &amp;gt;= 10%&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;pid &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;find_bot&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ts&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; DRAWDOWN LIMIT HIT -&amp;gt; bot stopped"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;2
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;                        &lt;span class="c"&gt;# one P&amp;amp;L line every 15 minutes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;monitor_pnl.py&lt;/code&gt; polls the local Freqtrade REST API (&lt;code&gt;/api/v1/profit&lt;/code&gt;, &lt;code&gt;/api/v1/status&lt;/code&gt;, &lt;code&gt;/api/v1/balance&lt;/code&gt;) and appends a single line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-09-25T15:02:58Z wallet=500.13 closed_pnl=+0.000USDT (+0.00%) all_pnl=-0.000USDT trades=0W/0L wr=0.0% open=1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you have a &lt;strong&gt;time series&lt;/strong&gt;, not a vibe. You can see the day the behaviour changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The comparison, honestly
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;systemd&lt;/th&gt;
&lt;th&gt;cron + watchdog&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;restart on crash&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;restart on reboot&lt;/td&gt;
&lt;td&gt;✅ (&lt;code&gt;enable&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;✅ (cron is persistent)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;needs root&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;detects "alive but stuck"&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ (health script)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P&amp;amp;L time series&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;enforced risk rule&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;moves to any machine in 2 files&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Neither is "better". systemd is the right tool if all you need is "keep the binary up" and you have root. The watchdog wins when you want &lt;strong&gt;evidence and a kill switch&lt;/strong&gt; — which, for anything handling money, is the actual requirement.&lt;/p&gt;

&lt;p&gt;You can also run both: systemd to survive reboots, the watchdog cron for health and risk. They do not conflict.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually kills unattended bots
&lt;/h2&gt;

&lt;p&gt;Ranked by how often I have seen them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The process dies and nobody notices&lt;/strong&gt; — the watchdog fixes this in the first five minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It runs for weeks and you never look at the data&lt;/strong&gt; — the P&amp;amp;L log fixes this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It draws down and you are asleep&lt;/strong&gt; — the kill switch fixes this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Machine reboots and nothing restarts it&lt;/strong&gt; — cron/&lt;code&gt;enable&lt;/code&gt; fixes this.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Notice that three of the four are observability and risk, not supervision.&lt;/p&gt;




&lt;p&gt;I packaged this exact layer — the &lt;code&gt;/proc&lt;/code&gt;-based watchdog, the P&amp;amp;L logger and the drawdown kill switch, with a dry-run config and setup guide — as the &lt;strong&gt;&lt;a href="https://tntofficial.gumroad.com/l/priklq" rel="noopener noreferrer"&gt;Freqtrade Self-Healing Starter Kit&lt;/a&gt;&lt;/strong&gt; ($9). It is the code from this post, ready to drop in, so you skip the hour I lost on the self-killing watchdog.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Related: &lt;a href="https://dev.to/matteo_adorni_948bc601a06/keep-any-long-running-python-process-alive-on-linux-for-0-the-proc-watchdog-pattern-1c9"&gt;Keep any long-running Python process alive on Linux for $0 (the /proc watchdog pattern)&lt;/a&gt; — the same pattern, generalised beyond Freqtrade.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Related in this series:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/matteo_adorni_948bc601a06/run-a-self-healing-crypto-trading-bot-247-for-0-freqtrade-watchdog-30dh"&gt;Run a self-healing crypto trading bot 24/7 for $0 (Freqtrade + watchdog)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/matteo_adorni_948bc601a06/keep-any-long-running-python-process-alive-on-linux-for-0-the-proc-watchdog-pattern-1c9"&gt;Keep any long-running Python process alive on Linux for $0 (the /proc watchdog pattern)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/matteo_adorni_948bc601a06/i-backtested-14-trading-strategies-only-one-passed-my-filters-4e8e"&gt;I backtested 14 trading strategies. Only one passed my filters.&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;The watchdog from this post is open source: **&lt;a href="https://github.com/moon-hacks/proc-watchdog" rel="noopener noreferrer"&gt;https://github.com/moon-hacks/proc-watchdog&lt;/a&gt;&lt;/em&gt;* (MIT).*&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Prefer someone to just do it? I also offer a **done-for-you setup&lt;/em&gt;* (&lt;a href="https://tntofficial.gumroad.com/l/vweza" rel="noopener noreferrer"&gt;https://tntofficial.gumroad.com/l/vweza&lt;/a&gt;) — Freqtrade installed, backtested and left running in dry-run with the watchdog active.*&lt;/p&gt;

</description>
      <category>trading</category>
      <category>linux</category>
      <category>devops</category>
      <category>python</category>
    </item>
    <item>
      <title>I backtested 14 trading strategies. Only one passed my filters.</title>
      <dc:creator>matteo adorni</dc:creator>
      <pubDate>Fri, 25 Sep 2026 15:20:14 +0000</pubDate>
      <link>https://dev.to/matteo_adorni_948bc601a06/i-backtested-14-trading-strategies-only-one-passed-my-filters-4e8e</link>
      <guid>https://dev.to/matteo_adorni_948bc601a06/i-backtested-14-trading-strategies-only-one-passed-my-filters-4e8e</guid>
      <description>&lt;p&gt;The first time I backtested a trading strategy, it returned a 100% win rate. Six trades. I almost shipped it.&lt;/p&gt;

&lt;p&gt;That number told me nothing, and neither does the number you get from any single backtest run. What matters is a &lt;strong&gt;filter&lt;/strong&gt; — a small set of criteria that data has to pass before you are allowed to believe it. This post is the filter, applied to 14 real Freqtrade strategies over a 200-day window with exchange fees included.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup (so you can reproduce it)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Data: 200 days of OHLCV for &lt;code&gt;BTC/USDT&lt;/code&gt;, &lt;code&gt;ETH/USDT&lt;/code&gt;, &lt;code&gt;SOL/USDT&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Fees: 0.1% worst-case, included in every result below.&lt;/li&gt;
&lt;li&gt;Window: 2026-03-10 → 2026-09-25, during which the market rose roughly &lt;strong&gt;+31%&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Engine: &lt;code&gt;freqtrade backtesting --timerange 20260310-20260925 --breakdown&lt;/code&gt;.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;freqtrade download-data &lt;span class="nt"&gt;--config&lt;/span&gt; config.json &lt;span class="nt"&gt;--days&lt;/span&gt; 200 &lt;span class="nt"&gt;--timeframes&lt;/span&gt; 5m 15m 1h
&lt;span class="k"&gt;for &lt;/span&gt;s &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;ls &lt;/span&gt;user_data/strategies/&lt;span class="k"&gt;*&lt;/span&gt;.py | xargs &lt;span class="nt"&gt;-n1&lt;/span&gt; &lt;span class="nb"&gt;basename&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/.py//'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;freqtrade backtesting &lt;span class="nt"&gt;--config&lt;/span&gt; config.json &lt;span class="nt"&gt;--strategy&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--timerange&lt;/span&gt; 20260310-20260925 &lt;span class="nt"&gt;--breakdown&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"logs/backtest_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.txt"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The filter
&lt;/h2&gt;

&lt;p&gt;Three rules. Every one exists because of a specific way a backtest lies.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Profit factor &amp;gt; 1.5.&lt;/strong&gt; Profit factor is gross profit ÷ gross loss. Below 1.5, a handful of lucky trades is doing all the work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Max drawdown &amp;lt; 15%.&lt;/strong&gt; A strategy you cannot psychologically survive is a strategy you will disable at the worst moment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Win rate &amp;gt; 55% &lt;em&gt;on a usable sample&lt;/em&gt;.&lt;/strong&gt; 100% on six trades is noise. I only count a strategy as a candidate above ~50 trades.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And one meta-rule: &lt;strong&gt;compare against buy-and-hold.&lt;/strong&gt; In a market that rose 31%, a strategy returning +1.6% did not beat the market. It just lost less badly than a coin flip. That does not make it worthless — it makes it &lt;em&gt;honest&lt;/em&gt;, and honesty is what you want in the thing that decides your entries.&lt;/p&gt;

&lt;h2&gt;
  
  
  The results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;strategy&lt;/th&gt;
&lt;th&gt;tf&lt;/th&gt;
&lt;th&gt;trades&lt;/th&gt;
&lt;th&gt;win%&lt;/th&gt;
&lt;th&gt;profit&lt;/th&gt;
&lt;th&gt;profit factor&lt;/th&gt;
&lt;th&gt;max drawdown&lt;/th&gt;
&lt;th&gt;Sharpe&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SwingHighToSky&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;15m&lt;/td&gt;
&lt;td&gt;108&lt;/td&gt;
&lt;td&gt;68.5%&lt;/td&gt;
&lt;td&gt;+1.60%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.21&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.37%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.27&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BbandRsi&lt;/td&gt;
&lt;td&gt;1h&lt;/td&gt;
&lt;td&gt;62&lt;/td&gt;
&lt;td&gt;74.2%&lt;/td&gt;
&lt;td&gt;+3.99%&lt;/td&gt;
&lt;td&gt;1.38&lt;/td&gt;
&lt;td&gt;7.92%&lt;/td&gt;
&lt;td&gt;0.62&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UniversalMACD&lt;/td&gt;
&lt;td&gt;5m&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;+1.48%&lt;/td&gt;
&lt;td&gt;∞&lt;/td&gt;
&lt;td&gt;0.00%&lt;/td&gt;
&lt;td&gt;0.79&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CombinedBinHAndCluc&lt;/td&gt;
&lt;td&gt;5m&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;+1.41%&lt;/td&gt;
&lt;td&gt;∞&lt;/td&gt;
&lt;td&gt;0.00%&lt;/td&gt;
&lt;td&gt;1.25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ClucMay72018&lt;/td&gt;
&lt;td&gt;5m&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;85.7%&lt;/td&gt;
&lt;td&gt;+0.51%&lt;/td&gt;
&lt;td&gt;6.52&lt;/td&gt;
&lt;td&gt;0.09%&lt;/td&gt;
&lt;td&gt;0.73&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read it the way the filter demands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;UniversalMACD&lt;/code&gt; and &lt;code&gt;CombinedBinHAndCluc&lt;/code&gt; are traps.&lt;/strong&gt; Perfect win rates, infinite profit factor — on 6 and 9 trades. An infinite profit factor means &lt;em&gt;zero losing trades happened&lt;/em&gt;, which on a tiny sample is a coincidence, not an edge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;BbandRsi&lt;/code&gt; earns the most in absolute terms&lt;/strong&gt; (+3.99%) but its profit factor of 1.38 fails rule 1. Big return, fragile distribution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SwingHighToSky&lt;/code&gt; is the only one passing all three rules&lt;/strong&gt; with a sample worth trusting: 108 trades, 68.5% win rate, profit factor 5.21, drawdown 0.37%.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One strategy out of fourteen. That ratio is the point — not that there is a winner, but that the filter is strict enough to make the winner meaningful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Sharpe column matters more than profit
&lt;/h2&gt;

&lt;p&gt;Profit tells you what happened. Sharpe tells you how rough the ride was. &lt;code&gt;SwingHighToSky&lt;/code&gt; returns less in absolute terms than &lt;code&gt;BbandRsi&lt;/code&gt; but with a Sharpe of 3.27 vs 0.62 — a far smoother equity curve. For a bot you intend to leave running unattended, smoothness is not a luxury; it is what stops you from turning it off.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did with this
&lt;/h2&gt;

&lt;p&gt;I did not pick the highest number. I picked the one that survived the filter, put it in dry-run, and left it running — because the next question is not "which strategy wins" but "can I keep it running, trusted, for two weeks". That is an infrastructure problem, not a strategy problem, and it is the part most backtests never reach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the process has to come back if it dies,&lt;/li&gt;
&lt;li&gt;the P&amp;amp;L has to be recorded so you can compare weeks,&lt;/li&gt;
&lt;li&gt;a risk rule has to be enforced by code, not by willpower.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I packaged exactly that layer — a self-healing watchdog, a P&amp;amp;L logger, and a hard drawdown kill switch — into a small kit so you can put the filter's winner into a survivable setup in about five minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;→ &lt;a href="https://tntofficial.gumroad.com/l/priklq" rel="noopener noreferrer"&gt;Freqtrade Self-Healing Starter Kit&lt;/a&gt;&lt;/strong&gt; ($9): the exact watchdog and config from this workflow, plus the setup guide.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Your backtest is a hypothesis generator, not an answer. Write the filter &lt;strong&gt;before&lt;/strong&gt; you look at the results — otherwise you will rationalise the number you like. And always ask what the market did over the same window; a strategy that loses to buy-and-hold in a bull market still might be worth running, but only if you know that is what you are choosing.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is part of a short series on running a trading bot you can actually leave alone.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;The infrastructure layer (watchdog + P&amp;amp;L logger + drawdown kill switch) is packaged here:&lt;/em&gt;&lt;br&gt;
&lt;strong&gt;&lt;a href="https://tntofficial.gumroad.com/l/priklq" rel="noopener noreferrer"&gt;Freqtrade Self-Healing Starter Kit&lt;/a&gt;&lt;/strong&gt; — $9.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Related in this series:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/matteo_adorni_948bc601a06/run-a-self-healing-crypto-trading-bot-247-for-0-freqtrade-watchdog-30dh"&gt;Run a self-healing crypto trading bot 24/7 for $0 (Freqtrade + watchdog)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/matteo_adorni_948bc601a06/keep-any-long-running-python-process-alive-on-linux-for-0-the-proc-watchdog-pattern-1c9"&gt;Keep any long-running Python process alive on Linux for $0 (the /proc watchdog pattern)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/matteo_adorni_948bc601a06/keep-freqtrade-running-247-systemd-vs-a-zero-dependency-watchdog-2d44"&gt;Keep Freqtrade running 24/7: systemd vs a zero-dependency watchdog&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;The watchdog from this post is open source: **&lt;a href="https://github.com/moon-hacks/proc-watchdog" rel="noopener noreferrer"&gt;https://github.com/moon-hacks/proc-watchdog&lt;/a&gt;&lt;/em&gt;* (MIT).*&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Prefer someone to just do it? I also offer a **done-for-you setup&lt;/em&gt;* (&lt;a href="https://tntofficial.gumroad.com/l/vweza" rel="noopener noreferrer"&gt;https://tntofficial.gumroad.com/l/vweza&lt;/a&gt;) — Freqtrade installed, backtested and left running in dry-run with the watchdog active.*&lt;/p&gt;

</description>
      <category>trading</category>
      <category>python</category>
      <category>crypto</category>
      <category>datascience</category>
    </item>
    <item>
      <title>Keep any long-running Python process alive on Linux for $0 (the /proc watchdog pattern)</title>
      <dc:creator>matteo adorni</dc:creator>
      <pubDate>Fri, 25 Sep 2026 15:01:43 +0000</pubDate>
      <link>https://dev.to/matteo_adorni_948bc601a06/keep-any-long-running-python-process-alive-on-linux-for-0-the-proc-watchdog-pattern-1c9</link>
      <guid>https://dev.to/matteo_adorni_948bc601a06/keep-any-long-running-python-process-alive-on-linux-for-0-the-proc-watchdog-pattern-1c9</guid>
      <description>&lt;p&gt;Anything you leave running will eventually stop running. A scraper, a trading bot, an agent, a queue worker — it does not matter how good the code is. What matters is whether you find out in five minutes or in three weeks.&lt;/p&gt;

&lt;p&gt;This is the pattern I use to make long-running processes self-healing, with no orchestrator, no systemd gymnastics, and no paid monitoring. It is a single bash script and a cron line. It works for anything: it does not need to know what your process does, only what it looks like.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap everyone falls into first
&lt;/h2&gt;

&lt;p&gt;The obvious watchdog is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; pgrep &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"mybot"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;restart_mybot
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks correct. It is a landmine.&lt;/p&gt;

&lt;p&gt;The problem: &lt;code&gt;pgrep -f&lt;/code&gt; matches against the &lt;strong&gt;full command line&lt;/strong&gt;, and your watchdog's own command line contains the pattern you are searching for. Depending on how the script is invoked (especially from cron or from a parent that echoes the command), the script can match &lt;strong&gt;itself&lt;/strong&gt; and conclude the process is alive — or worse, &lt;code&gt;pkill -f mybot&lt;/code&gt; will kill the shell that is running the watchdog.&lt;/p&gt;

&lt;p&gt;I hit this exact bug: the watchdog ran, matched its own invocation, and killed itself before doing anything. The log looked like the bot was fine. It was not.&lt;/p&gt;

&lt;p&gt;There is a second, subtler failure: matching a &lt;em&gt;name&lt;/em&gt; is not matching a &lt;em&gt;process&lt;/em&gt;. Any unrelated binary with the same substring satisfies the check.&lt;/p&gt;

&lt;h2&gt;
  
  
  The robust check: scan &lt;code&gt;/proc&lt;/code&gt;, and require two things
&lt;/h2&gt;

&lt;p&gt;Instead of matching a string, walk &lt;code&gt;/proc&lt;/code&gt; and require &lt;strong&gt;both&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the command line contains your exact start command, &lt;strong&gt;and&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;the process's resolved executable is the one from &lt;strong&gt;your&lt;/strong&gt; virtualenv.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;VENV_PY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;readlink&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;/.venv/bin/python"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

find_bot&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;p &lt;span class="k"&gt;in&lt;/span&gt; /proc/[0-9]&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nv"&gt;pid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="p"&gt;#/proc/&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="s2"&gt;/cmdline"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;continue
    &lt;/span&gt;&lt;span class="nv"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'\0'&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &amp;lt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="s2"&gt;/cmdline"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;continue
    case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cmd&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
      &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="s2"&gt;"freqtrade trade --config"&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nv"&gt;exe&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;readlink&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="s2"&gt;/exe"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$exe&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VENV_PY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;esac&lt;/span&gt;
  &lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why this is safe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/proc/&amp;lt;pid&amp;gt;/cmdline&lt;/code&gt;&lt;/strong&gt; is the process's actual argv, NUL-separated — not a shell's mangled string.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;readlink /proc/&amp;lt;pid&amp;gt;/exe&lt;/code&gt;&lt;/strong&gt; resolves to the real binary. Requiring it to equal your venv's interpreter means you can never match a stray system Python, a shell, or the watchdog itself.&lt;/li&gt;
&lt;li&gt;A process that has already died leaves no &lt;code&gt;/proc/&amp;lt;pid&amp;gt;&lt;/code&gt; entry, so there is no stale-match window.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adapt the &lt;code&gt;case&lt;/code&gt; pattern and the &lt;code&gt;exe&lt;/code&gt; path and this same function works for a Node app, a Go binary, a scraper — anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full watchdog
&lt;/h2&gt;

&lt;p&gt;This is production-shaped: it restarts the process, records a health snapshot on every run, enforces a safety rule, and &lt;strong&gt;stays silent unless it acts&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Generic self-healing watchdog. Silent on success.&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;span class="nv"&gt;BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/home/kali/myapp
&lt;span class="nv"&gt;VENV_PY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;readlink&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;/.venv/bin/python"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"watchdog: cannot cd &lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nv"&gt;LOG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;/logs/watchdog.log
ts&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +&lt;span class="s2"&gt;"%Y-%m-%dT%H:%M:%SZ"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

find_proc&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;p &lt;span class="k"&gt;in&lt;/span&gt; /proc/[0-9]&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nv"&gt;pid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="p"&gt;#/proc/&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="s2"&gt;/cmdline"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;continue
    &lt;/span&gt;&lt;span class="nv"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'\0'&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &amp;lt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="s2"&gt;/cmdline"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;continue
    case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cmd&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
      &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="s2"&gt;"myapp run --config"&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nv"&gt;exe&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;readlink&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="s2"&gt;/exe"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$exe&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VENV_PY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;esac&lt;/span&gt;
  &lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;MSG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find_proc&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ts&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; process not running -&amp;gt; restarting"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;nohup&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;/.venv/bin/python"&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; myapp run &lt;span class="nt"&gt;--config&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;/config.json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;/logs/app_stdout.log"&lt;/span&gt; 2&amp;gt;&amp;amp;1 &amp;amp;
  &lt;span class="nb"&gt;sleep &lt;/span&gt;30                     &lt;span class="c"&gt;# give it time to come up&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find_proc&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;MSG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"watchdog: restarted at &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ts&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ts&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; restart FAILED"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1                     &lt;span class="c"&gt;# exit non-zero so cron mails / your notifier fires&lt;/span&gt;
  &lt;span class="k"&gt;fi
fi&lt;/span&gt;

&lt;span class="c"&gt;# health snapshot + safety rule (exit codes: 0 ok, 2 breach, 3 unreachable)&lt;/span&gt;
&lt;span class="nv"&gt;OUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VENV_PY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;/health.py"&lt;/span&gt; 2&amp;gt;&amp;amp;1&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;RC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$RC&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 2 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  for &lt;/span&gt;pid &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;find_proc&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ts&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; SAFETY RULE TRIPPED -&amp;gt; stopping process"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;2
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MSG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MSG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;       &lt;span class="c"&gt;# speak only when something happened&lt;/span&gt;
&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why "silent on success" is the important part
&lt;/h3&gt;

&lt;p&gt;A watchdog that messages you every fifteen minutes gets muted within a day — and a muted watchdog is worse than none. Emit output &lt;strong&gt;only&lt;/strong&gt; when you restarted something or a rule tripped, and pipe that output to whatever notifier you already use (a Discord webhook, &lt;code&gt;mail&lt;/code&gt;, a Telegram bot). Silence means health.&lt;/p&gt;

&lt;h3&gt;
  
  
  The safety rule lives outside your app
&lt;/h3&gt;

&lt;p&gt;Whatever invariant protects you from disaster — a loss limit for a trading bot, an error-rate ceiling for a scraper, a queue-depth threshold — put it in the watchdog's health script, in &lt;strong&gt;one&lt;/strong&gt; place. Your app's internal guards protect a request; this protects the &lt;em&gt;account&lt;/em&gt;. One threshold, one exit code, one kill switch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scheduling
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;*&lt;/span&gt;/15 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /home/kali/myapp/scripts/watchdog.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fifteen minutes is a good default: fast enough to matter, slow enough to be invisible. Make the script idempotent — two runs must not start two copies. The two-condition check above guarantees that: if the process exists, the function returns a PID and nothing is started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test it, or you do not have one
&lt;/h2&gt;

&lt;p&gt;A watchdog you have never seen fire is a watchdog you do not have. Do this once, immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. kill the app&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;pid &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;pgrep &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"myapp run --config"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="c"&gt;# 2. run the watchdog by hand&lt;/span&gt;
bash scripts/watchdog.sh
&lt;span class="c"&gt;# 3. confirm BOTH the restart and the log line&lt;/span&gt;
&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-3&lt;/span&gt; logs/watchdog.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it does not restart, you have a path or an &lt;code&gt;exe&lt;/code&gt; mismatch — fix it now, not at 03:00.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost
&lt;/h2&gt;

&lt;p&gt;Zero. A cron line, a bash script, and the filesystem you already have. No agent to install, no dashboard, no token, no monthly fee. For the 95% of solo projects that just need "it keeps running and tells me when it doesn't", this pattern beats a full orchestrator on every axis that matters: it is 60 lines you can read and reason about completely.&lt;/p&gt;

&lt;p&gt;The two ideas worth keeping, whatever you build: &lt;strong&gt;identify processes by &lt;code&gt;/proc&lt;/code&gt; + &lt;code&gt;exe&lt;/code&gt;, never by name&lt;/strong&gt;, and &lt;strong&gt;let the watchdog speak only when it acts&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Want this pre-built?&lt;/strong&gt; I packaged the watchdog, the P&amp;amp;L logger, the backtest runner and the config&lt;br&gt;
into a drop-in kit — &lt;strong&gt;&lt;a href="https://tntofficial.gumroad.com/l/priklq" rel="noopener noreferrer"&gt;Freqtrade Self-Healing Starter Kit&lt;/a&gt;&lt;/strong&gt; ($9).&lt;br&gt;
It is the exact code from this article plus a setup guide with the &lt;code&gt;/proc&lt;/code&gt; gotcha already solved,&lt;br&gt;
so you can go from zero to a self-healing bot in about five minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Related in this series:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/matteo_adorni_948bc601a06/run-a-self-healing-crypto-trading-bot-247-for-0-freqtrade-watchdog-30dh"&gt;Run a self-healing crypto trading bot 24/7 for $0 (Freqtrade + watchdog)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/matteo_adorni_948bc601a06/i-backtested-14-trading-strategies-only-one-passed-my-filters-4e8e"&gt;I backtested 14 trading strategies. Only one passed my filters.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/matteo_adorni_948bc601a06/keep-freqtrade-running-247-systemd-vs-a-zero-dependency-watchdog-2d44"&gt;Keep Freqtrade running 24/7: systemd vs a zero-dependency watchdog&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;The watchdog from this post is open source: **&lt;a href="https://github.com/moon-hacks/proc-watchdog" rel="noopener noreferrer"&gt;https://github.com/moon-hacks/proc-watchdog&lt;/a&gt;&lt;/em&gt;* (MIT).*&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Prefer someone to just do it? I also offer a **done-for-you setup&lt;/em&gt;* (&lt;a href="https://tntofficial.gumroad.com/l/vweza" rel="noopener noreferrer"&gt;https://tntofficial.gumroad.com/l/vweza&lt;/a&gt;) — Freqtrade installed, backtested and left running in dry-run with the watchdog active.*&lt;/p&gt;

</description>
      <category>linux</category>
      <category>bash</category>
      <category>devops</category>
      <category>python</category>
    </item>
    <item>
      <title>Run a self-healing crypto trading bot 24/7 for $0 (Freqtrade + watchdog)</title>
      <dc:creator>matteo adorni</dc:creator>
      <pubDate>Fri, 25 Sep 2026 14:35:55 +0000</pubDate>
      <link>https://dev.to/matteo_adorni_948bc601a06/run-a-self-healing-crypto-trading-bot-247-for-0-freqtrade-watchdog-30dh</link>
      <guid>https://dev.to/matteo_adorni_948bc601a06/run-a-self-healing-crypto-trading-bot-247-for-0-freqtrade-watchdog-30dh</guid>
      <description>&lt;p&gt;Most "run a trading bot" tutorials stop at &lt;em&gt;backtest passed, bot started&lt;/em&gt;. 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.&lt;/p&gt;

&lt;p&gt;This post is the part nobody writes: a &lt;strong&gt;production-shaped, zero-cost setup&lt;/strong&gt; where the bot restarts itself, logs its own P&amp;amp;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a watchdog matters more than the strategy
&lt;/h2&gt;

&lt;p&gt;A strategy is a hypothesis. A watchdog is what turns a hypothesis into data.&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Availability&lt;/strong&gt; — the process is restarted automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt; — a P&amp;amp;L snapshot is appended to a log on every cycle, so you have a time series instead of a vibe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Risk&lt;/strong&gt; — a global rule (e.g. &lt;em&gt;stop if drawdown ≥ 10% of the wallet&lt;/em&gt;) is enforced by code, not by willpower.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Linux (or WSL2) with ~3 GB RAM.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;uv&lt;/code&gt; for the Python environment. On modern distros &lt;code&gt;pip install&lt;/code&gt; into the system Python is blocked by PEP 668, and &lt;code&gt;uv&lt;/code&gt; sidesteps that cleanly.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-LsSf&lt;/span&gt; https://astral.sh/uv/install.sh | sh
uv venv ~/freqtrade/.venv
&lt;span class="nb"&gt;source&lt;/span&gt; ~/freqtrade/.venv/bin/activate
uv pip &lt;span class="nb"&gt;install &lt;/span&gt;freqtrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A minimal, sane config
&lt;/h2&gt;

&lt;p&gt;Start with a cosmetic config and tune later. The important bits for a safe dry-run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"max_open_trades"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stake_currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USDT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stake_amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dry_run"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dry_run_wallet"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"trading_mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"spot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exchange"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"binance"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"secret"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pair_whitelist"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"BTC/USDT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ETH/USDT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SOL/USDT"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pairlists"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"StaticPairList"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"api_server"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"listen_ip_address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"127.0.0.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"listen_port"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hermes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"password"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CHANGE_ME"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"jwt_secret_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CHANGE_ME_TOO"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ws_token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CHANGE_ME_AS_WELL"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bot_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dryrun"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"initial_state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"running"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two rules that will save you hours:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bind the API to &lt;code&gt;127.0.0.1&lt;/code&gt;&lt;/strong&gt;, never &lt;code&gt;0.0.0.0&lt;/code&gt;. The API exposes order and config operations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set &lt;code&gt;jwt_secret_key&lt;/code&gt;, &lt;code&gt;ws_token&lt;/code&gt; and a real password&lt;/strong&gt; even in dry-run. Empty secrets are a production foot-gun waiting to be copied into live.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Backtest before you believe anything
&lt;/h2&gt;

&lt;p&gt;Download history and backtest several strategies in their &lt;em&gt;native&lt;/em&gt; 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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;freqtrade download-data &lt;span class="nt"&gt;--config&lt;/span&gt; config.json &lt;span class="nt"&gt;--days&lt;/span&gt; 200 &lt;span class="nt"&gt;--timeframes&lt;/span&gt; 5m 15m 1h

&lt;span class="k"&gt;for &lt;/span&gt;s &lt;span class="k"&gt;in &lt;/span&gt;SwingHighToSky BbandRsi UniversalMACD&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;freqtrade backtesting &lt;span class="nt"&gt;--config&lt;/span&gt; config.json &lt;span class="nt"&gt;--strategy&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--timerange&lt;/span&gt; 20260310-20260925 &lt;span class="nt"&gt;--breakdown&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A real result set from a 200-day window (fees included) looked like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;strategy&lt;/th&gt;
&lt;th&gt;timeframe&lt;/th&gt;
&lt;th&gt;trades&lt;/th&gt;
&lt;th&gt;win%&lt;/th&gt;
&lt;th&gt;profit&lt;/th&gt;
&lt;th&gt;profit factor&lt;/th&gt;
&lt;th&gt;max drawdown&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SwingHighToSky&lt;/td&gt;
&lt;td&gt;15m&lt;/td&gt;
&lt;td&gt;108&lt;/td&gt;
&lt;td&gt;68.5%&lt;/td&gt;
&lt;td&gt;+1.60%&lt;/td&gt;
&lt;td&gt;5.21&lt;/td&gt;
&lt;td&gt;0.37%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BbandRsi&lt;/td&gt;
&lt;td&gt;1h&lt;/td&gt;
&lt;td&gt;62&lt;/td&gt;
&lt;td&gt;74.2%&lt;/td&gt;
&lt;td&gt;+3.99%&lt;/td&gt;
&lt;td&gt;1.38&lt;/td&gt;
&lt;td&gt;7.92%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UniversalMACD&lt;/td&gt;
&lt;td&gt;5m&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;+1.48%&lt;/td&gt;
&lt;td&gt;∞&lt;/td&gt;
&lt;td&gt;0.00%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read that table honestly. &lt;code&gt;UniversalMACD&lt;/code&gt; shows a perfect win rate — on &lt;strong&gt;six trades&lt;/strong&gt;. That is not an edge, that is a sample size. &lt;code&gt;BbandRsi&lt;/code&gt; has the highest absolute return but a weak profit factor. &lt;code&gt;SwingHighToSky&lt;/code&gt; is the only one passing &lt;em&gt;all three&lt;/em&gt; filters (PF &amp;gt; 1.5, drawdown &amp;lt; 15%, win rate &amp;gt; 55%) on a statistically usable sample. Pick on the &lt;em&gt;quality of the evidence&lt;/em&gt;, not on the biggest number.&lt;/p&gt;

&lt;p&gt;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 &lt;em&gt;operational behaviour&lt;/em&gt;, not to beat the market. Be suspicious of any backtest that does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The watchdog (the actually interesting part)
&lt;/h2&gt;

&lt;p&gt;Here is a working watchdog, designed to run from cron and stay silent unless something is wrong.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# - restarts the bot if the process died&lt;/span&gt;
&lt;span class="c"&gt;# - appends a P&amp;amp;L snapshot to logs/daily_pnl.txt on every run&lt;/span&gt;
&lt;span class="c"&gt;# - SILENT on success; speaks only on restart or risk-limit breach&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;span class="nv"&gt;BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/home/kali/freqtrade
&lt;span class="nv"&gt;VENV_PY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;readlink&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;/.venv/bin/python"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"watchdog: cannot cd &lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nv"&gt;PY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;/.venv/bin/python
&lt;span class="nv"&gt;LOG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;/logs/watchdog.log
ts&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +&lt;span class="s2"&gt;"%Y-%m-%dT%H:%M:%SZ"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# Print PIDs of the REAL bot processes (empty if none).&lt;/span&gt;
find_bot&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;p &lt;span class="k"&gt;in&lt;/span&gt; /proc/[0-9]&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nv"&gt;pid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="p"&gt;#/proc/&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="s2"&gt;/cmdline"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;continue
    &lt;/span&gt;&lt;span class="nv"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'\0'&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &amp;lt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="s2"&gt;/cmdline"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;continue
    case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cmd&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
      &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="s2"&gt;"freqtrade trade --config"&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nv"&gt;exe&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;readlink&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="s2"&gt;/exe"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$exe&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VENV_PY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;esac&lt;/span&gt;
  &lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;MSG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find_bot&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ts&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; bot not running -&amp;gt; restarting"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;nohup&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;/.venv/bin/freqtrade"&lt;/span&gt; trade &lt;span class="nt"&gt;--config&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;/config.json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--strategy&lt;/span&gt; SwingHighToSky &lt;span class="nt"&gt;--dry-run&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;/logs/trade_stdout.log"&lt;/span&gt; 2&amp;gt;&amp;amp;1 &amp;amp;
  &lt;span class="nb"&gt;sleep &lt;/span&gt;30
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find_bot&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;MSG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"crypto-bot: process was gone, restarted at &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ts&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;."&lt;/span&gt;
  &lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ts&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; restart FAILED"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
  &lt;span class="k"&gt;fi
fi

&lt;/span&gt;&lt;span class="nv"&gt;OUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;/monitor_pnl.py"&lt;/span&gt; 2&amp;gt;&amp;amp;1&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;RC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$RC&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 2 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;                      &lt;span class="c"&gt;# drawdown limit hit&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;pid &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;find_bot&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ts&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; DRAWDOWN LIMIT HIT -&amp;gt; stopping bot"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"crypto-bot: drawdown &amp;gt;= 10% of wallet. Bot STOPPED by risk rule."&lt;/span&gt; 
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;2
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MSG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MSG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The gotcha that cost me an hour
&lt;/h3&gt;

&lt;p&gt;Do &lt;strong&gt;not&lt;/strong&gt; detect the process with &lt;code&gt;pkill -f freqtrade&lt;/code&gt; or &lt;code&gt;pgrep -f freqtrade&lt;/code&gt;. That pattern also matches the shell that invoked your script — because the string &lt;code&gt;freqtrade&lt;/code&gt; appears in its command line — and the script kills itself. I hit exactly this. Scanning &lt;code&gt;/proc&lt;/code&gt; and requiring &lt;strong&gt;both&lt;/strong&gt; the &lt;code&gt;freqtrade trade --config&lt;/code&gt; cmdline &lt;strong&gt;and&lt;/strong&gt; an &lt;code&gt;exe&lt;/code&gt; pointing into the venv is unambiguous and safe.&lt;/p&gt;

&lt;h3&gt;
  
  
  Make it run without babysitting
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# every 15 minutes; quiet unless it acts&lt;/span&gt;
&lt;span class="k"&gt;*&lt;/span&gt;/15 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /home/kali/.hermes/scripts/crypto_watchdog.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify it end-to-end the first time: kill the bot, run the watchdog, confirm the restart &lt;em&gt;and&lt;/em&gt; the P&amp;amp;L line. A watchdog you have never seen fire is a watchdog you do not have.&lt;/p&gt;

&lt;h2&gt;
  
  
  The kill switch is the whole point
&lt;/h2&gt;

&lt;p&gt;The drawdown rule (&lt;code&gt;&amp;gt;= 10% of the wallet → stop the bot&lt;/code&gt;) is enforced outside the strategy, in the watchdog. Strategy-level stoplosses protect a &lt;em&gt;trade&lt;/em&gt;; this protects the &lt;em&gt;account&lt;/em&gt;. Keeping it in one place — not scattered across strategy parameters — means you can reason about your worst case at a glance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results after the first hours
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wallet=500.10 closed_pnl=+0.000USDT (+0.00%) all_pnl=+0.003USDT (+0.00%) trades=0W/0L open=1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero closed trades, one open position, equity essentially flat. That is the correct &lt;em&gt;shape&lt;/em&gt; for hour one of a dry-run. The value is not the P&amp;amp;L yet — it is that the clock is now running on a validated, self-healing, risk-capped system, at zero cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do next
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Let it run for two weeks.&lt;/strong&gt; Judge the &lt;em&gt;behaviour&lt;/em&gt;: did the watchdog ever fire? did the kill switch ever trip? how did it behave around a volatility spike?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hyperopt deliberately&lt;/strong&gt; — a handful of epochs with a Sharpe-based loss, then re-backtest. Do not hyperopt your way into a curve fit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only then&lt;/strong&gt; consider real capital — and only with an amount you are genuinely fine losing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The whole setup costs nothing and removes the two failure modes that kill most hobby bots: a dead process and an unenforced risk rule.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you build on this, the two files worth stealing are the &lt;code&gt;/proc&lt;/code&gt;-based process check and the drawdown kill switch. Everything else is configuration.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Want this pre-built?&lt;/strong&gt; I packaged the watchdog, the P&amp;amp;L logger, the backtest runner and the config&lt;br&gt;
into a drop-in kit — &lt;strong&gt;&lt;a href="https://tntofficial.gumroad.com/l/priklq" rel="noopener noreferrer"&gt;Freqtrade Self-Healing Starter Kit&lt;/a&gt;&lt;/strong&gt; ($9).&lt;br&gt;
It is the exact code from this article plus a setup guide with the &lt;code&gt;/proc&lt;/code&gt; gotcha already solved,&lt;br&gt;
so you can go from zero to a self-healing bot in about five minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Related in this series:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/matteo_adorni_948bc601a06/keep-any-long-running-python-process-alive-on-linux-for-0-the-proc-watchdog-pattern-1c9"&gt;Keep any long-running Python process alive on Linux for $0 (the /proc watchdog pattern)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/matteo_adorni_948bc601a06/i-backtested-14-trading-strategies-only-one-passed-my-filters-4e8e"&gt;I backtested 14 trading strategies. Only one passed my filters.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/matteo_adorni_948bc601a06/keep-freqtrade-running-247-systemd-vs-a-zero-dependency-watchdog-2d44"&gt;Keep Freqtrade running 24/7: systemd vs a zero-dependency watchdog&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;The watchdog from this post is open source: **&lt;a href="https://github.com/moon-hacks/proc-watchdog" rel="noopener noreferrer"&gt;https://github.com/moon-hacks/proc-watchdog&lt;/a&gt;&lt;/em&gt;* (MIT).*&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Prefer someone to just do it? I also offer a **done-for-you setup&lt;/em&gt;* (&lt;a href="https://tntofficial.gumroad.com/l/vweza" rel="noopener noreferrer"&gt;https://tntofficial.gumroad.com/l/vweza&lt;/a&gt;) — Freqtrade installed, backtested and left running in dry-run with the watchdog active.*&lt;/p&gt;

</description>
      <category>crypto</category>
      <category>python</category>
      <category>trading</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
