discord: retire per-trade pings, add daily sharp digest; trust: v2 self-certify + invalidated-wallet fallback

* per-trade Discord pings removed everywhere: copybot placement/settle/startup
  alerts silenced (engine.webhook forced off), webhook_receiver.py archived,
  Procfile removed. The ONLY Discord output is now live/discord_daily.py at the
  end of the daily pipeline: one embed listing every watch_sharps wallet with a
  Polymarket profile link + 30D conviction win% / record / P&L.

* trust.py fixes found on the first scheduled run (sharp list collapsed 25->7):
  - Polymarket rewrites endDate after resolution, so freshly re-pulled wallets'
    corrected res_t stopped matching the stale cross-wallet consensus
    (374/454 of iohihoo's rows wrongly distrusted). v2 rows with resolved=TRUE
    are now self-certifying (resolved=TRUE implies endDate-based res_t observed
    post-end) — consensus only gates legacy rows.
  - daily.sh invalidates watchlist wallets by DELETING their pulled row; a
    transiently failed re-pull then dropped the wallet's entire history from
    the trusted set (the 0x73afc816 whale vanished). Legacy rows of wallets
    missing from pulled now fall back to trusting markets resolved >=14d ago.
  Re-run: 85 conviction wallets, 34 copy-positive sharps — whale/iohihoo/
  Stavenson/LSB1/Kruto all recovered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jaxperro
2026-07-04 18:31:30 -04:00
parent 24b64ba4c1
commit 8c07343735
10 changed files with 2072 additions and 338 deletions
+2 -1
View File
@@ -48,7 +48,8 @@ verified. Fix anything red before arming.
It prints the caps, asks for the confirmation phrase, then baselines (no
retro-copying) and waits for the next conviction trade from Kruto2027 /
shisan888 / fortuneking / LSB1. Every placement pings Discord. Watch the first
shisan888 / fortuneking / LSB1. Placements log to the console/Railway logs (per-trade Discord
pings were retired 2026-07-04). Watch the first
fill: order accepted → shares in the account (Polymarket UI) → on resolution,
SETTLE line + auto-redeem tx hash.
-1
View File
@@ -1 +0,0 @@
web: python3 webhook_receiver.py
+4 -4
View File
@@ -25,7 +25,7 @@ Three deployed pieces + one static dashboard:
|-------|--------------|--------------|
| **daily pipeline** (`live/daily.sh`) | this Mac, launchd 10:00 | refresh the bet cache → 5-gate skill scan → fee-aware sharp selection → conviction floors → backtest book → publish JSON feeds to GitHub |
| **copybot worker** (`copybot.py` via `host/start.sh`) | Railway, 24/7 | polls the 4 followed wallets every 60s, paper-copies their conviction bets with real fees/lag/slippage accounting, settles at CLOB resolution, commits its book back to the repo |
| **Discord watcher** (`webhook_receiver.py`) | Railway `web` service | Alchemy address-activity webhook → instant trade pings |
| **Discord digest** (`live/discord_daily.py`) | end of the daily pipeline | one message/day: the sharp list with profile links + 30-day conviction stats (per-trade pings retired 2026-07-04; the old Alchemy watcher lives in `archive/webhook_receiver.py`) |
| **dashboard** | [jaxperro.com/trading](https://jaxperro.com/trading) (static, in the `jaxperro` repo) | renders the three JSON feeds: live bot book, backtest book, sharp table |
**The July 2026 live test:** a fresh $1,000 paper book (started 2026-07-02, on
@@ -59,7 +59,7 @@ follows (see [`LIVE_TEST.md`](LIVE_TEST.md)).
| `LIVE_TEST.md` · `preflight_live.py` · `redeem.py` | real-money runbook, read-only credential preflight, on-chain redemption |
| `insider.py` | the original detector: z-score, pre-resolution timing, fresh-wallet flags, funding-cluster rings |
| `smart_money.py` | shared HTTP helper + survivorship-corrected win-rate dashboard (`:8899`) |
| `webhook_receiver.py` | Alchemy webhook → Discord trade pings (Railway `web` service) |
| `archive/webhook_receiver.py` | retired 2026-07-04: Alchemy webhook → per-trade Discord pings (replaced by `live/discord_daily.py`'s daily digest) |
| `wide/` | frozen-subgraph bulk scanner (1.76M wallets, historical only — subgraph froze Jan 2026) |
| `archive/` | the six strategies that didn't work, kept honest ([archive/README](archive/README.md)) |
| `hunt.py` · `huntwide.py` · `oos.py` · `copyback.py` | earlier research sweeps/backtests (superseded by `live/`) |
@@ -127,9 +127,9 @@ in [`live/README.md`](live/README.md).
| file | holds |
|------|-------|
| `config.json` | Discord webhooks, Alchemy key, the followed-wallet list + per-wallet conviction floors (auto-refreshed daily by `sync_floors.py`) |
| `config.json` | `daily_webhook` (the Discord digest), Alchemy key, the followed-wallet list + per-wallet conviction floors (auto-refreshed daily by `sync_floors.py`) |
| `config.live.json` | live-trading credentials (`private_key`, `funder_address`) + tiny test caps — see `LIVE_TEST.md` |
| Railway `copybot` service | `GITHUB_TOKEN` (fine-grained PAT, contents-RW on this repo — the bot commits its state/feed back), optional `DISCORD_WEBHOOK` |
| Railway `copybot` service | `GITHUB_TOKEN` (fine-grained PAT, contents-RW on this repo — the bot commits its state/feed back), `DISCORD_WEBHOOK` no longer used (per-trade pings retired) |
---
+4 -13
View File
@@ -53,7 +53,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "archive"))
from copytrade import ( # noqa: E402
CopyTrader, PaperExecutor, LiveExecutor, DEFAULT_CONFIG,
load_json, save_json, new_state, recent_trades, post_discord, confirm_live,
load_json, save_json, new_state, recent_trades, confirm_live,
)
from smart_money import SSL_CTX # noqa: E402
@@ -287,8 +287,8 @@ class Copybot:
self.engine = engine
self.filt = filt
self.redeemer = redeemer # gap 2: on-chain redeem of resolved positions (live)
# webhook from env (so a scheduled runner can inject it as a secret) or config
self.webhook = os.environ.get("DISCORD_WEBHOOK") or cfg.get("discord_webhook", "")
# trade-by-trade Discord pings retired 2026-07-04: the only Discord
# output now is the daily sharp digest (live/discord_daily.py)
self.names = {}
for w in cfg.get("watch", []):
self.names[w["wallet"].lower()] = w.get("name", w["wallet"][:10])
@@ -748,6 +748,7 @@ def main():
executor = LedgerPaperExecutor() # tracks cash flows for realized-P&L reporting
engine = CopyTrader(cfg, state, executor, args.state)
engine.webhook = "" # per-trade Discord alerts retired — daily digest only
filt = FollowFilter(cfg)
bot = Copybot(cfg, engine, filt, redeemer=redeemer)
@@ -794,11 +795,6 @@ def main():
# run works today without the deployed Alchemy webhook. (Production push uses the
# webhook below; behaviour through the filter+engine is identical either way.)
if args.poll:
if bot.webhook:
post_discord(bot.webhook,
f"✅ **Copybot paper run started** ({mode}, poll {args.poll}s)\n"
f"{len(cfg.get('watchlist', []))} wallets · {filt.describe()}\n"
f"${cfg['bankroll_usd']:.0f} book · ${cfg['bankroll_usd']*cfg['bankroll_pct']:.0f}/bet")
bot.baseline()
log(f"poll mode · every {args.poll}s · Ctrl-C to stop")
bot.write_feed() # publish an initial "online" snapshot
@@ -821,11 +817,6 @@ def main():
signing_key = (os.environ.get("ALCHEMY_SIGNING_KEY")
or cfg.get("alchemy_signing_key", ""))
port = int(os.environ.get("PORT", 8080))
if bot.webhook:
post_discord(bot.webhook,
f"✅ **Copybot online** ({mode})\n"
f"push-driven · {len(cfg.get('watchlist', []))} wallets · "
f"{filt.describe()}\nYou'll get a ping on every trade it places.")
log(f"listening on :{port} · POST /alchemy · "
f"signature-verify {'ON' if signing_key else 'OFF'}")
ThreadingHTTPServer(("0.0.0.0", port), make_handler(bot, signing_key)).serve_forever()
File diff suppressed because it is too large Load Diff
+4 -23
View File
@@ -60,26 +60,7 @@ elif git commit -q -m "live: daily refresh — skilled + sharp wallets [skip ci]
fi
echo "[daily] $(date '+%F %T') done -> watch_sharps.json + dashboard.html"
# ping Discord (webhook kept in gitignored ../config.json -> daily_webhook)
PUBLISH="$PUBLISH" python3 - <<'PY'
import json, os, ssl, time, urllib.request
try: # cwd is live/ (daily.sh cd's there); config is repo-root
hook = json.load(open("../config.json")).get("daily_webhook")
except Exception:
hook = None
if hook:
try:
n = len(json.load(open("watch_sharps.json")))
except Exception:
n = "?"
msg = (f"✅ Sharp pipeline finished {time.strftime('%Y-%m-%d %H:%M')} — "
f"{n} copyable sharps · feed {os.environ.get('PUBLISH','?')}")
data = json.dumps({"content": msg}).encode()
req = urllib.request.Request(hook, data=data,
headers={"Content-Type": "application/json", "User-Agent": "Mozilla/5.0"}) # Discord 403s w/o UA
try:
urllib.request.urlopen(req, timeout=15, context=ssl._create_unverified_context())
print("[daily] discord pinged")
except Exception as e:
print("[daily] discord ping failed:", e)
PY
# Discord: the daily sharp-list digest (profile links + 30D conviction stats).
# The only Discord output in the system — per-trade pings retired 2026-07-04.
# Webhook lives in gitignored ../config.json -> daily_webhook.
python3 discord_daily.py "feed: $PUBLISH" || echo "[daily] discord digest failed (non-fatal)"
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""Daily Discord digest: the copyable-sharp list with 30-day conviction stats.
Replaces the retired trade-by-trade pings (archive/webhook_receiver.py and the
copybot placement alerts, removed 2026-07-04): ONE message per daily run,
listing every wallet in watch_sharps.json with a Polymarket profile link and
its 30-day conviction win %, record, and conviction P&L — the same conv30_*
fields the dashboard renders (the wallet's own position stats, not the copy
book). Links require an embed: Discord ignores markdown links in plain
`content` messages.
Webhook: `daily_webhook` in the gitignored repo-root config.json.
python3 discord_daily.py "feed pushed" # optional arg shown in the footer
"""
import json
import os
import ssl
import sys
import time
import urllib.request
HERE = os.path.dirname(os.path.abspath(__file__))
MAX_ROWS = 30 # embed description caps at 4096 chars; 30 rows fits
def main():
try:
hook = json.load(open(os.path.join(HERE, "..", "config.json"))).get("daily_webhook")
except Exception:
hook = None
if not hook:
print("[discord] no daily_webhook in ../config.json — skipping")
return
try:
sharps = json.load(open(os.path.join(HERE, "watch_sharps.json")))
except Exception as e:
print(f"[discord] watch_sharps.json unreadable ({e}) — skipping")
return
sharps.sort(key=lambda s: s.get("conv30_pnl") or 0, reverse=True)
lines = []
for i, s in enumerate(sharps[:MAX_ROWS], 1):
name = (s.get("name") or s["wallet"][:10])
name = name.replace("[", "(").replace("]", ")")[:24] # keep the md link intact
url = "https://polymarket.com/profile/" + s["wallet"]
if s.get("conv30_win") is None:
stats = "30D: no conviction bets"
else:
stats = (f"**{s['conv30_win']:.0f}%** · "
f"{s.get('conv30_won', 0)}-{s.get('conv30_lost', 0)} · "
f"**${s.get('conv30_pnl', 0):+,.0f}**")
lines.append(f"`{i:>2}` [{name}]({url}) — {stats}")
embed = {
"title": f"Daily sharps · {len(sharps)} copyable · {time.strftime('%Y-%m-%d')}",
"description": "\n".join(lines)[:4000],
"color": 0x3FD17A,
"footer": {"text": ((sys.argv[1] + " · ") if len(sys.argv) > 1 else "")
+ "30D win% · conviction record · conviction P&L (wallet's own)"},
}
req = urllib.request.Request(
hook, data=json.dumps({"embeds": [embed]}).encode(),
headers={"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0"}) # Discord 403s the default UA
try:
urllib.request.urlopen(req, timeout=15, context=ssl._create_unverified_context())
print(f"[discord] daily sharp digest sent ({min(len(sharps), MAX_ROWS)} rows)")
except Exception as e:
print("[discord] digest failed:", e)
if __name__ == "__main__":
main()
+31 -16
View File
@@ -12,14 +12,25 @@ Two ways a cached bet row can lie (see FINDINGS.md "The holder blind spot"):
the market resolved; rows pulled earlier carry a price mark, and v2 rows
say so via resolved=False, but legacy rows can't.
The fix is cross-wallet consensus: endDate-based rows for one market agree on
the same res_t across every wallet, while ts-fallback rows scatter (each
wallet's sell time is its own). So a row is TRUSTED iff
A row is TRUSTED iff res_t <= now AND either:
* its res_t equals the market's modal res_t across >= 2 distinct wallets (E)
* E <= now (the market is actually over)
* the wallet's pulled_at >= E (won observed after resolution -> 0/1)
* resolved IS DISTINCT FROM FALSE (v2 mark rows out)
* **v2 self-certifying**: resolved = TRUE. resolved is only True when the
pull saw a real endDate AND the market had ended, so res_t is endDate-based
(never the ts fallback) and won was observed post-end. No consensus needed —
important because Polymarket sometimes REWRITES endDate after resolution,
so a freshly-pulled wallet's corrected res_t stops matching the stale
cross-wallet mode (2026-07-04: this wrongly distrusted 374/454 of iohihoo's
rows the morning after his re-pull).
* **legacy consensus** (resolved IS NULL, pre-v2 rows): res_t equals the
market's modal res_t across >= 2 distinct wallets (endDate rows agree;
ts-fallback sell times scatter), and the wallet's pulled_at >= E so won was
observed after resolution. If the wallet is missing from `pulled` (daily.sh
invalidates watchlist wallets by DELETING their pulled row; a transiently
failed re-pull then leaves them absent), fall back to trusting markets
resolved >= 14 days ago — every wallet gets re-pulled at least each
MAX_AGE_DAYS cycle, so its rows' true pull time is at most ~2 weeks old.
(resolved = FALSE rows — price marks on unended markets — are never trusted.)
~13.5M of 19.2M rows pass; what's dropped is exactly the poison that made
scalpers look like 99%-win holders. Selection must only ever score trusted rows.
@@ -42,10 +53,12 @@ tr_cons AS (SELECT cond, res_t AS E FROM (
FROM tr_r GROUP BY cond, res_t) WHERE rn = 1 AND nw >= 2),
trusted AS (
SELECT tr_r.* FROM tr_r
JOIN tr_cons ON tr_r.cond = tr_cons.cond AND tr_r.res_t = tr_cons.E
JOIN pulled pl ON pl.wallet = tr_r.wallet
WHERE tr_cons.E <= {now} AND pl.pulled_at >= tr_cons.E
AND (tr_r.resolved IS DISTINCT FROM FALSE))
LEFT JOIN tr_cons ON tr_r.cond = tr_cons.cond AND tr_r.res_t = tr_cons.E
LEFT JOIN pulled pl ON pl.wallet = tr_r.wallet
WHERE tr_r.res_t <= {now} AND (
tr_r.resolved = TRUE
OR (tr_r.resolved IS NULL AND tr_cons.E IS NOT NULL
AND tr_cons.E <= COALESCE(pl.pulled_at, {now} - 1209600))))
"""
@@ -81,11 +94,13 @@ def trusted_wallet_rows(runq, wallet, now=None):
SELECT DISTINCT b.cond, b.asset, b.won,
least(0.999, greatest(0.001, b.p)) p, b.res_t, b.size
FROM bets b
JOIN t_cons c ON b.cond = c.cond AND b.res_t = c.E
JOIN pulled pl ON pl.wallet = b.wallet
WHERE b.wallet = ? AND b.size > 0 AND c.E <= ?
AND pl.pulled_at >= c.E AND (b.resolved IS DISTINCT FROM FALSE)""",
[wallet, now])
LEFT JOIN t_cons c ON b.cond = c.cond AND b.res_t = c.E
LEFT JOIN pulled pl ON pl.wallet = b.wallet
WHERE b.wallet = ? AND b.size > 0 AND b.res_t > 0 AND b.res_t <= ?
AND (b.resolved = TRUE
OR (b.resolved IS NULL AND c.E IS NOT NULL
AND c.E <= COALESCE(pl.pulled_at, ? - 1209600)))""",
[wallet, now, now])
def conviction_record(runq, wallet, days=90, pctile=0.80, now=None):
+1045 -100
View File
File diff suppressed because it is too large Load Diff