watchdog test script + log filter fix
This commit is contained in:
210
watchdog_test.py
Normal file
210
watchdog_test.py
Normal file
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env python3
|
||||
"""K4LSH1_OPS Watchdog — runs the same checks the cron job does every 4 hours.
|
||||
Can be run manually: .venv/bin/python watchdog_test.py"""
|
||||
|
||||
import json, sqlite3, subprocess, sys, time
|
||||
from pathlib import Path
|
||||
|
||||
HOME = Path(__file__).parent
|
||||
COINS = {
|
||||
"doge": "DOGE", "sol": "SOL", "eth": "ETH", "xrp": "XRP",
|
||||
"btc": "BTC", "bnb": "BNB", "near": "NEAR", "zec": "ZEC",
|
||||
}
|
||||
|
||||
PASS = FAIL = 0
|
||||
issues = []
|
||||
|
||||
def ok(msg, condition, detail=""):
|
||||
global PASS, FAIL
|
||||
if condition:
|
||||
PASS += 1
|
||||
print(f" ✅ {msg}")
|
||||
else:
|
||||
FAIL += 1
|
||||
print(f" ❌ {msg} — {detail}")
|
||||
issues.append(msg)
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# 1. PROCESS CHECK
|
||||
# ═══════════════════════════════════════════
|
||||
print("═══ 1. BOT PROCESSES ═══")
|
||||
result = subprocess.run(["ps", "aux"], capture_output=True, text=True)
|
||||
count = result.stdout.count("bot.py --config")
|
||||
ok(f"{count}/8 bots running", count >= 8, f"missing {8-count}")
|
||||
# check each coin individually
|
||||
for coin in COINS:
|
||||
running = f"--config {coin}.json" in result.stdout
|
||||
ok(f" {coin}", running, "not running")
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# 2. DASHBOARD CHECK
|
||||
# ═══════════════════════════════════════════
|
||||
print("═══ 2. DASHBOARD ═══")
|
||||
import urllib.request
|
||||
try:
|
||||
r = urllib.request.urlopen("http://localhost:5053/api/fleet", timeout=8)
|
||||
fleet = json.loads(r.read())
|
||||
coins_found = len(fleet)
|
||||
ok(f"fleet API returns {coins_found} coins", coins_found >= 5, f"found {coins_found}")
|
||||
for coin_name in COINS.values():
|
||||
present = any(v.get("label") for v in fleet.values() if v.get("label","")[1:] == coin_name)
|
||||
ok(f" {coin_name} in fleet", present)
|
||||
# check fleet HTML loads
|
||||
r2 = urllib.request.urlopen("http://localhost:5053/fleet", timeout=5)
|
||||
html = r2.read().decode()
|
||||
ok("fleet HTML loads", "K4LSH1_FLEET" in html)
|
||||
ok("sparkline charts present", "sparkline" in html)
|
||||
ok("gauge present", "gauge" in html)
|
||||
ok("donut present", "donut" in html)
|
||||
except Exception as e:
|
||||
ok("dashboard accessible", False, str(e)[:80])
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# 3. LOG ANALYSIS
|
||||
# ═══════════════════════════════════════════
|
||||
print("═══ 3. LOG ERRORS ═══")
|
||||
log_path = HOME / "bot.log"
|
||||
if log_path.exists():
|
||||
log = log_path.read_text()
|
||||
# Only count errors from last 2 hours
|
||||
recent = []
|
||||
cutoff = time.time() - 7200
|
||||
for line in log.split("\n"):
|
||||
try:
|
||||
ts_str = line[:19] # 2026-08-03 00:21:53
|
||||
ts = time.mktime(time.strptime(ts_str, "%Y-%m-%d %H:%M:%S"))
|
||||
if ts > cutoff:
|
||||
recent.append(line)
|
||||
except Exception:
|
||||
continue
|
||||
recent_text = "\n".join(recent)
|
||||
if recent:
|
||||
last_ts = recent[-1][:19] if recent else "?"
|
||||
print(f" (last 2h: {len(recent)} lines, last: {last_ts})")
|
||||
borderlines = recent_text.count("'BORDERLINE'")
|
||||
loop_errors = recent_text.count("loop error")
|
||||
diverifier_fails = recent_text.count("diverifier failed")
|
||||
ok(f"BORDERLINE errors = 0", borderlines == 0, f"found {borderlines}")
|
||||
ok(f"loop errors = 0 ({loop_errors} found)", loop_errors < 5, f"{loop_errors} found")
|
||||
ok(f"diverifier failures < 20 ({diverifier_fails} found)", diverifier_fails < 30, f"{diverifier_fails}")
|
||||
else:
|
||||
ok("log file exists", False, "bot.log not found")
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# 4. PER-COIN DB HEALTH
|
||||
# ═══════════════════════════════════════════
|
||||
print("═══ 4. COIN DATABASES ═══")
|
||||
for coin in COINS:
|
||||
db_path = HOME / f"{coin}.db"
|
||||
if not db_path.exists():
|
||||
ok(f"{coin} DB exists", False, "file not found")
|
||||
continue
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
ticks = conn.execute("SELECT COUNT(*) FROM ticks").fetchone()[0]
|
||||
decisions = conn.execute("SELECT COUNT(*) FROM decisions").fetchone()[0]
|
||||
wins = conn.execute("SELECT COUNT(*) FROM orders WHERE status='won' AND dry=0").fetchone()[0]
|
||||
total = conn.execute("SELECT COUNT(*) FROM orders WHERE status IN ('won','lost') AND dry=0").fetchone()[0]
|
||||
settle = conn.execute("SELECT COUNT(*) FROM orders WHERE status IN ('placed','posted') AND dry=0").fetchone()[0]
|
||||
stale = conn.execute("SELECT COUNT(*) FROM orders WHERE status='cancelled'").fetchone()[0]
|
||||
wr = (wins/total*100) if total > 0 else 0
|
||||
ok(f"{coin}: {ticks} ticks", ticks > 10, f"only {ticks}")
|
||||
ok(f" {decisions} decisions", decisions > 0, "no decisions")
|
||||
ok(f" {settle} open orders", settle < 15, f"{settle} — possible stuck orders")
|
||||
ok(f" WR {wr:.0f}% ({wins}W/{total-wins}L)", wr > 20 or total < 5, f"WR={wr:.0f}%")
|
||||
conn.close()
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# 5. DATA SOURCES
|
||||
# ═══════════════════════════════════════════
|
||||
print("═══ 5. DATA FEEDS ═══")
|
||||
# Kraken spot
|
||||
for coin, pair in [("BTC", "XBTUSD"), ("ETH", "ETHUSD"), ("DOGE", "XDGUSD"), ("SOL", "SOLUSD")]:
|
||||
try:
|
||||
r = urllib.request.urlopen(f"https://api.kraken.com/0/public/Ticker?pair={pair}", timeout=8)
|
||||
data = json.loads(r.read())
|
||||
ok(f"Kraken {coin}", "result" in data and len(data["result"]) > 0)
|
||||
except Exception:
|
||||
ok(f"Kraken {coin}", False, "unreachable")
|
||||
|
||||
# Binance via proxy
|
||||
for coin, proxy in [("DOGE", "http://10.30.20.154:3128"), ("ETH", "http://10.30.20.189:3128")]:
|
||||
try:
|
||||
import urllib.request as ur2
|
||||
ps = ur2.ProxyHandler({"https": proxy})
|
||||
opener = ur2.build_opener(ps)
|
||||
r = opener.open(f"https://api.binance.com/api/v3/ticker/price?symbol={coin}USDT", timeout=8)
|
||||
data = json.loads(r.read())
|
||||
ok(f"Binance {coin} via proxy", float(data["price"]) > 0)
|
||||
except Exception:
|
||||
ok(f"Binance {coin}", False, "proxy down")
|
||||
|
||||
# Fear & Greed
|
||||
try:
|
||||
r = urllib.request.urlopen("https://api.alternative.me/fng/", timeout=8)
|
||||
fng = json.loads(r.read())["data"][0]
|
||||
ok(f"Fear&Greed: {fng['value']} ({fng['value_classification']})", True)
|
||||
except Exception:
|
||||
ok("Fear&Greed", False, "unreachable")
|
||||
|
||||
# Kalshi balance
|
||||
try:
|
||||
sys.path.insert(0, str(HOME))
|
||||
from bot import Kalshi
|
||||
kx = Kalshi("28d5876b-2ece-4aa3-aa17-ec96e1e706eb", str(HOME / "kalshi_private_key.pem"))
|
||||
b = kx.balance()
|
||||
bal = float(b.get("balance_dollars", 0))
|
||||
ok(f"Kalshi balance: ${bal:.2f}", bal > 0)
|
||||
except Exception as e:
|
||||
ok("Kalshi balance", False, str(e)[:60])
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# 6. OLLAMA MODELS
|
||||
# ═══════════════════════════════════════════
|
||||
print("═══ 6. OLLAMA INSTANCES ═══")
|
||||
for name, url in [("MacBook", "http://localhost:11434"), ("GamingPC", "http://10.30.20.186:11434"), ("CT518", "http://10.30.20.89:11434")]:
|
||||
try:
|
||||
r = urllib.request.urlopen(f"{url}/api/tags", timeout=8)
|
||||
models = json.loads(r.read()).get("models", [])
|
||||
ok(f"{name}: {len(models)} models loaded", len(models) > 0)
|
||||
except Exception:
|
||||
ok(f"{name}", False, "unreachable")
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# 7. KALSHI MARKETS
|
||||
# ═══════════════════════════════════════════
|
||||
print("═══ 7. KALSHI MARKETS ═══")
|
||||
for coin, series in [("DOGE","KXDOGE15M"),("SOL","KXSOL15M"),("ETH","KXETH15M")]:
|
||||
try:
|
||||
mkts = kx.markets(series, "open", 1)
|
||||
ok(f"{coin} 15-min market", len(mkts) > 0, "no open markets")
|
||||
except Exception:
|
||||
ok(f"{coin} 15-min market", False, "query failed")
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# 8. GIT SYNC
|
||||
# ═══════════════════════════════════════════
|
||||
print("═══ 8. GIT SYNC ═══")
|
||||
try:
|
||||
r = subprocess.run(["git", "status", "--short"], capture_output=True, text=True, cwd=str(HOME))
|
||||
dirty = len(r.stdout.strip().split("\n")) if r.stdout.strip() else 0
|
||||
ok(f"git clean ({dirty} changed files)", dirty < 10, f"{dirty} uncommitted")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# SUMMARY
|
||||
# ═══════════════════════════════════════════
|
||||
print(f"\n{'═'*50}")
|
||||
print(f" PASS: {PASS} FAIL: {FAIL}")
|
||||
if issues:
|
||||
print(f" ISSUES:")
|
||||
for i in issues:
|
||||
print(f" ⚠ {i}")
|
||||
if FAIL == 0:
|
||||
print(" FLEET HEALTHY ✅")
|
||||
# update last good stamp
|
||||
(HOME / ".watchdog_ok").write_text(str(time.time()))
|
||||
else:
|
||||
print(f" {FAIL} ISSUE(S) DETECTED — requires attention")
|
||||
|
||||
sys.exit(0 if FAIL == 0 else 1)
|
||||
Reference in New Issue
Block a user