trade-copilot / analysis /parse_alerts.py
utkarshpathak48's picture
fix: featured card shows real PA confluence grade
c4eac91
Raw
History Blame Contribute Delete
3.86 kB
#!/usr/bin/env python3
"""parse_alerts.py — extract Trade Copilot alerts from a Telegram Desktop JSON export.
Usage: python3 parse_alerts.py result.json [--out alerts.csv]
Input: Telegram Desktop > bot chat > Export chat history > JSON (result.json)
Output: alerts.csv (ts_utc, iso, symbol, direction, conf, entry, sl, tp1, tp2, tp3)
Stdlib only. Handles TG's mixed text arrays (strings + entity dicts), skips
heartbeats/tests/skip-notices, dedups same symbol+direction within 4h (keeps first).
"""
import json, re, csv, sys, datetime
DEDUP_WINDOW_S = 4 * 3600
RE_HEAD = re.compile(r'(?:🟢|🔴)?\s*([A-Z0-9]{1,15}-USDT)\s+(LONG|SHORT)\s*·\s*([\d.]+)\s*/\s*10')
RE_ENTRY = re.compile(r'Entry\s+([\d.,]+)')
RE_SL = re.compile(r'\bSL\s+([\d.,]+)')
RE_TP1 = re.compile(r'TP1\s+([\d.,]+)')
RE_TP2 = re.compile(r'TP2\s+([\d.,]+)')
RE_TP3 = re.compile(r'TP3\s+([\d.,]+)')
SKIP_MARKERS = ("Trade Copilot alive", "Test Message", "Test:", "Skipped", "funds in use",
"NOT executed", "PROFIT", "LOSS", "ENTERED", "LIQUIDATED", "summary")
def flatten_text(t):
"""TG export 'text' is str OR list of str/{'text':...} — flatten to one string."""
if isinstance(t, str):
return t
out = []
for part in t:
out.append(part if isinstance(part, str) else str(part.get("text", "")))
return "".join(out)
def num(s):
return float(s.replace(",", ""))
def main():
if len(sys.argv) < 2:
sys.exit("usage: python3 parse_alerts.py result.json [--out alerts.csv]")
src = sys.argv[1]
out = sys.argv[sys.argv.index("--out") + 1] if "--out" in sys.argv else "alerts.csv"
with open(src, encoding="utf-8") as f:
data = json.load(f)
messages = data.get("messages", data if isinstance(data, list) else [])
rows, skipped, last_seen = [], 0, {}
for m in messages:
if m.get("type") != "message":
continue
text = flatten_text(m.get("text", ""))
if not text or any(k in text for k in SKIP_MARKERS):
continue
h = RE_HEAD.search(text)
e, s_, t1 = RE_ENTRY.search(text), RE_SL.search(text), RE_TP1.search(text)
if not (h and e and s_ and t1):
continue
# timestamp: prefer unixtime if present
if m.get("date_unixtime"):
ts = int(m["date_unixtime"])
else:
# TG export date is local time of the exporting machine; treat as-is, note in README
ts = int(datetime.datetime.fromisoformat(m["date"]).timestamp())
sym, direction, conf = h.group(1), h.group(2), float(h.group(3))
key = f"{sym}:{direction}"
if key in last_seen and ts - last_seen[key] < DEDUP_WINDOW_S:
skipped += 1
continue
last_seen[key] = ts
t2, t3 = RE_TP2.search(text), RE_TP3.search(text)
rows.append({
"ts_utc": ts,
"iso": datetime.datetime.fromtimestamp(ts, datetime.timezone.utc).isoformat(),
"symbol": sym, "direction": direction, "conf": conf,
"entry": num(e.group(1)), "sl": num(s_.group(1)), "tp1": num(t1.group(1)),
"tp2": num(t2.group(1)) if t2 else "", "tp3": num(t3.group(1)) if t3 else "",
})
rows.sort(key=lambda r: r["ts_utc"])
with open(out, "w", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=list(rows[0].keys()) if rows else
["ts_utc","iso","symbol","direction","conf","entry","sl","tp1","tp2","tp3"])
w.writeheader()
w.writerows(rows)
print(f"parsed {len(rows)} alerts (deduped {skipped}) → {out}")
if rows:
print(f"range: {rows[0]['iso']}{rows[-1]['iso']}")
hi = [r for r in rows if r["conf"] >= 8.8]
print(f"conf ≥ 8.8: {len(hi)} alerts")
if __name__ == "__main__":
main()