mirror of
https://github.com/jaxperro/winning-wallet-finder.git
synced 2026-07-27 15:57:47 +00:00
5c8ee5961b
The nightly Mac-coupled bulk ingest was the fragile link (today: Mac slept through 08:00, the pipeline started 09:31, and the sftp bulk pull wedged in timeout cascades holding the db lock — digest blocked behind it). Stage 0 moves the fold onto the box and reduces the Mac to an incremental mirror: - recorder/fold.py (sidecar, capture stays PID 1): closed gz segments -> zstd parquet /data/parquet/<fam>/date=*/segment.parquet, row-parity verified, manifest-logged, THEN gz deleted. Deletion invariant STRONGER: raw needs a verified parquet; parquet needs a Mac ACK before the disk guard may prune it. duckdb capped 384MB; VM 256MB -> 1GB. - recorder/sync_tape.py (com.jaxperro.tape-sync every 15 min + daily.sh): manifest-driven incremental sftp pull, per-file row verify, append into rtds.duckdb NATIVE tables (views-over-parquet rejected: sim's per-asset point queries would crawl), segment-keyed idempotence shared with the legacy ingest.py (kept as fallback), ack back to the box. - recorder/bootstrap_parquet.py: pre-fold history exported to the mirror, parity OK (13.84M trades + 3.07M aux). live/parquet/ is now the complete durable layer Stage 1 (MotherDuck/ClickHouse) would consume. - research/tape.py connect(): brief retry — the 15-min sync holds the write lock for seconds. First run: backlog folded in <60s on the box, 36/36 files mirrored+ verified, rtds.duckdb 13.8M -> 18.2M trades, tape age 24h+ -> ~15-45 min. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
138 lines
7.5 KiB
Bash
Executable File
138 lines
7.5 KiB
Bash
Executable File
#!/bin/bash
|
|
# Daily skilled-wallet refresh, cache-backed:
|
|
# 1) discover — enumerate traders of markets resolved in the last ~14 days,
|
|
# merged into the accumulating candidate pool
|
|
# 2) freshen — force a re-pull of the current watchlists (skilled + sharps,
|
|
# for forward tracking), then top up the cache (collect re-pulls
|
|
# only new + >14d-stale wallets, so it's cheap after the first run)
|
|
# 3) re-score — the 5-gate skill funnel, instant from cache -> watch_skilled.json
|
|
# 4) sharps — conviction-profile scan + last-minute timing gate ->
|
|
# conviction_wallets.json + watch_sharps.json (the set the live
|
|
# trading dashboard reads via raw.githubusercontent)
|
|
# 5) floors — pin the live paper bot's per-wallet p80 conviction floors
|
|
# (trusted-cache p80, same as the backtest) into
|
|
# copybot.paper.json, so the bot and the backtest gate on the
|
|
# SAME threshold and boots are deterministic (no per-boot
|
|
# data-api drift). Committed in the publish step.
|
|
# 6) dashboard — regenerate + snapshot for auditable forward history
|
|
# 7) publish — commit + push the refreshed outputs so the live dashboard
|
|
# (jaxperro.com/trading) picks up the new sharp list
|
|
#
|
|
# Schedule with launchd/cron (Mac must be awake). Logs to daily.log.
|
|
set -u
|
|
# Run from a private copy so editing this file mid-run can't shift bash's read
|
|
# offset (the 2026-07-06 crash: a commit during step 2 made bash resume mid-word
|
|
# and die on a phantom syntax error). DAILY_SRC keeps paths anchored here.
|
|
if [ -z "${DAILY_COPY:-}" ]; then
|
|
DAILY_SRC="$(cd "$(dirname "$0")" && pwd)"
|
|
DAILY_COPY="$(mktemp "${TMPDIR:-/tmp}/daily.sh.XXXXXX")"
|
|
cp "$0" "$DAILY_COPY"
|
|
export DAILY_SRC DAILY_COPY
|
|
exec /bin/bash "$DAILY_COPY" "$@"
|
|
fi
|
|
LOCKDIR="${TMPDIR:-/tmp}/wwf-daily.lock.d"
|
|
if ! mkdir "$LOCKDIR" 2>/dev/null; then
|
|
echo "[daily] another run holds the lock ($LOCKDIR) — exiting"; exit 0
|
|
fi
|
|
trap 'rmdir "$LOCKDIR" 2>/dev/null; rm -f "$DAILY_COPY"' EXIT
|
|
cd "$DAILY_SRC"
|
|
# heads-up ping so the run's start is visible in Discord (digest comes at the end)
|
|
python3 discord_daily.py --ping "🔄 Daily pipeline started $(date '+%H:%M') — refreshing the bet cache (takes a while); sharp digest lands when it finishes." || true
|
|
echo "[daily] $(date '+%F %T') 1/6 discover (enumerate last 14d)"
|
|
python3 enumerate.py 14
|
|
echo "[daily] $(date '+%F %T') 2/6 freshen cache (watchlists forced + new wallets)"
|
|
python3 -c "import json,os,cache
|
|
wl=[]
|
|
for f in ('watch_skilled.json','watch_sharps.json'):
|
|
if os.path.exists(f):
|
|
wl += [w['wallet'] for w in json.load(open(f))]
|
|
if wl:
|
|
cache.invalidate(wl)" 2>/dev/null || true
|
|
python3 collect.py
|
|
echo "[daily] $(date '+%F %T') 3/6 re-score skilled (cache-backed, instant)"
|
|
python3 skill.py
|
|
echo "[daily] $(date '+%F %T') 4/7 sharps: conviction scan + last-minute timing gate"
|
|
python3 conviction_scan.py
|
|
python3 validate_timing.py
|
|
echo "[daily] $(date '+%F %T') 5/7 floors: pin copy-bot p80 conviction floors -> copybot.paper.json"
|
|
python3 sync_floors.py || echo "[daily] floor sync skipped"
|
|
# parity guard (2026-07-13 audit): class_pct lives in BOTH copybot.paper.json
|
|
# and backtest.json with no generator — warn if they silently desync so the
|
|
# paper book and backtest don't drift apart on sizing.
|
|
python3 - <<'PY' || true
|
|
import json
|
|
p=json.load(open("copybot.paper.json")).get("follow",{}).get("class_pct")
|
|
b=json.load(open("backtest.json")).get("class_pct")
|
|
if p!=b: print(f"[daily] ⚠ class_pct DESYNC: paper {p} != backtest {b}")
|
|
PY
|
|
echo "[daily] $(date '+%F %T') portfolio: cache-based \$1k book -> portfolio.json"
|
|
python3 portfolio.py || echo "[daily] portfolio skipped"
|
|
echo "[daily] $(date '+%F %T') calibration: live-book vs model, one row/day"
|
|
# The number that sizes real money (FINDINGS "The calibration experiment"):
|
|
# the measured ratio between the live paper book and the backtest of the SAME
|
|
# set. Live side from the bot-committed feed on GitHub (freshest truth; the
|
|
# local checkout can lag), model side from the portfolio.json just written.
|
|
python3 - <<'CALIB' || echo "[daily] calibration row skipped"
|
|
import csv, json, os, ssl, time, urllib.request
|
|
ctx = ssl._create_unverified_context()
|
|
live = json.load(urllib.request.urlopen(
|
|
"https://raw.githubusercontent.com/jaxperro/winning-wallet-finder/main/live/copybot_live.json",
|
|
timeout=30, context=ctx))
|
|
model = json.load(open("portfolio.json"))
|
|
row = {
|
|
"date": time.strftime("%F"),
|
|
"live_equity": round((live.get("cash") or 0) + (live.get("deployed") or 0)
|
|
+ (live.get("reserve") or 0), 2),
|
|
"live_realized": live.get("realized"), "live_open": live.get("open_count"),
|
|
"live_drift": live.get("ledger_drift"),
|
|
"model_equity": model.get("equity"), "model_realized": model.get("realized"),
|
|
"model_bank": model.get("bank"), "wallets": ",".join(live.get("wallets") or []),
|
|
}
|
|
os.makedirs("history", exist_ok=True)
|
|
path = "history/calibration.csv"
|
|
new = not os.path.exists(path)
|
|
with open(path, "a", newline="") as fh:
|
|
w = csv.DictWriter(fh, fieldnames=list(row))
|
|
if new:
|
|
w.writeheader()
|
|
w.writerow(row)
|
|
print(f"[daily] calibration: live ${row['live_equity']:,} vs model "
|
|
f"${row['model_equity']:,} -> {path}")
|
|
CALIB
|
|
echo "[daily] $(date '+%F %T') edge: parity-era per-signal edge vs fee hurdle -> history/edge.csv"
|
|
# The bankroll-decision number (HANDOFF rev 13 / 2026-07-16): one row/day;
|
|
# the verdict line rides the Discord digest footer below.
|
|
python3 edge.py || echo "[daily] edge skipped"
|
|
echo "[daily] $(date '+%F %T') tape: sync parquet mirror -> live/rtds.duckdb"
|
|
# Stage-0 warehouse (2026-07-21): the box folds segments to parquet itself;
|
|
# we mirror + append (recorder/sync_tape.py, also every 15 min via
|
|
# com.jaxperro.tape-sync). Old bulk ingest.py stays as the fallback for a
|
|
# fold-less recorder (it no-ops when fold has already eaten the segments).
|
|
python3 -u ../recorder/sync_tape.py || python3 ../recorder/ingest.py \
|
|
|| echo "[daily] tape sync skipped"
|
|
echo "[daily] $(date '+%F %T') 6/7 dashboard"
|
|
python3 dashboard.py
|
|
mkdir -p history && cp watch_skilled.json "history/watch_$(date '+%Y%m%d').json" 2>/dev/null
|
|
echo "[daily] $(date '+%F %T') 7/7 publish (commit + push refreshed outputs)"
|
|
PUBLISH="no changes"
|
|
git add watch_skilled.json watch_sharps.json conviction_wallets.json dashboard.html portfolio.json copybot.paper.json 2>/dev/null
|
|
if git diff --cached --quiet 2>/dev/null; then
|
|
echo "[daily] no output changes to publish"
|
|
elif git commit -q -m "live: daily refresh — skilled + sharp wallets [skip ci]"; then
|
|
# sync first so a diverged remote (e.g. a manual commit) doesn't wedge the
|
|
# auto-push permanently; abort a conflicting rebase and retry next run.
|
|
git pull --rebase --autostash -q origin main 2>/dev/null || git rebase --abort 2>/dev/null || true
|
|
if git push -q origin main; then
|
|
echo "[daily] pushed refreshed sharp list"; PUBLISH="pushed"
|
|
else
|
|
echo "[daily] push failed — committed locally, will retry next run"; PUBLISH="push failed (committed locally)"
|
|
fi
|
|
fi
|
|
echo "[daily] $(date '+%F %T') done -> watch_sharps.json + dashboard.html"
|
|
|
|
# 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.
|
|
EDGE_LINE="$(head -1 edge_verdict.txt 2>/dev/null || true)"
|
|
python3 discord_daily.py "feed: $PUBLISH${EDGE_LINE:+ · $EDGE_LINE}" || echo "[daily] discord digest failed (non-fatal)"
|