mirror of
https://github.com/jaxperro/winning-wallet-finder.git
synced 2026-07-27 15:57:47 +00:00
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:
+2
-1
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
+908
-180
File diff suppressed because it is too large
Load Diff
+4
-23
@@ -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)"
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user