valuebot: paper V0 — sub-2¢ portfolio, honest FAK fills, chain settles, own Fly app (VALUE silo)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
# wwf-valuebot — the VALUE silo's own Fly app (value/PLAN.md). No http_service:
|
||||
# nothing probes it, nothing auto-restarts it under a state surgery.
|
||||
app = "wwf-valuebot"
|
||||
primary_region = "arn"
|
||||
|
||||
[build]
|
||||
dockerfile = "value/fly.Dockerfile"
|
||||
|
||||
[[restart]]
|
||||
policy = "always"
|
||||
|
||||
[[vm]]
|
||||
size = "shared-cpu-1x"
|
||||
memory = "256mb"
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Stub tests for the value paper bot (value/PLAN.md V0): honest FAK fill
|
||||
model, event cap, cooldown, refund-aware chain settlement. No network.
|
||||
Run: python3 tests/test_valuebot.py
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
"..", "value"))
|
||||
import valuebot as vb # noqa: E402
|
||||
|
||||
vb.STATE = "/tmp/vb_test_state.json"
|
||||
vb.FILLS = "/tmp/vb_test_fills.jsonl"
|
||||
vb.FEED = "/tmp/vb_test_feed.json"
|
||||
|
||||
fails = []
|
||||
|
||||
|
||||
def case(name):
|
||||
def deco(fn):
|
||||
ba0, op0 = vb.book_asks, vb.onchain_payouts
|
||||
for f in (vb.STATE, vb.FILLS, vb.FEED):
|
||||
if os.path.exists(f):
|
||||
os.remove(f)
|
||||
try:
|
||||
fn()
|
||||
print(f"PASS {name}")
|
||||
except AssertionError as e:
|
||||
print(f"FAIL {name}: {e}")
|
||||
fails.append(name)
|
||||
finally:
|
||||
vb.book_asks, vb.onchain_payouts = ba0, op0
|
||||
return deco
|
||||
|
||||
|
||||
def cand(tok, event="ev-2026-07-17", mark=0.01):
|
||||
return {"token": tok, "outcome": "Yes", "mark": mark, "cond": "0xc" + tok,
|
||||
"title": f"mkt {tok}", "end": "2026-01-01T00:00:00Z", # already past
|
||||
"cat": "esports", "event": event, "tok_index": 0, "n_outcomes": 2}
|
||||
|
||||
|
||||
@case("fill model: walks the ladder inside the band, honest miss when thin")
|
||||
def t1():
|
||||
# $1 at 1c needs 100 shares; 60 at 0.010 + 50 at 0.0104 (inside 1.05 band)
|
||||
sh, px, r = vb.model_fill([(0.010, 60), (0.0104, 50)], 1.0, 0.02)
|
||||
assert r is None and abs(sh - (0.6/0.010 + 0.4/0.0104)) < 1e-6
|
||||
# thin: only $0.30 inside the band -> MISS
|
||||
sh, px, r = vb.model_fill([(0.010, 30), (0.02, 1000)], 1.0, 0.02)
|
||||
assert sh is None and "FAK no-match" in r, r
|
||||
# best ask above the bucket -> not a candidate
|
||||
sh, px, r = vb.model_fill([(0.03, 1000)], 1.0, 0.02)
|
||||
assert sh is None and "above" in r
|
||||
# empty/failed book
|
||||
assert vb.model_fill([], 1.0, 0.02)[2] == "no asks on the book"
|
||||
assert vb.model_fill(None, 1.0, 0.02)[2] == "book fetch failed"
|
||||
|
||||
|
||||
@case("band: a 1.9c best ask does NOT walk into 2.5c levels")
|
||||
def t2():
|
||||
sh, px, r = vb.model_fill([(0.019, 30), (0.025, 1000)], 1.0, 0.02)
|
||||
assert sh is None, "walked past min(MAX_PX, best*1.05)"
|
||||
|
||||
|
||||
@case("event cap 1: second ticket on the same event is skipped")
|
||||
def t3():
|
||||
st = vb.load_state()
|
||||
vb.book_asks = lambda tok: [(0.01, 1000)]
|
||||
vb.open_positions(st, [cand("A", "ev-2026-07-17"),
|
||||
cand("B", "ev-2026-07-17"),
|
||||
cand("C", "other-2026-07-18")], budget=10)
|
||||
assert set(st["my_pos"]) == {"A", "C"}, set(st["my_pos"])
|
||||
assert st["stats"]["fills"] == 2 and st["stats"]["attempts"] == 2
|
||||
|
||||
|
||||
@case("cooldown: a missed token is not re-checked inside COOLDOWN_S")
|
||||
def t4():
|
||||
st = vb.load_state()
|
||||
vb.book_asks = lambda tok: [(0.01, 10)] # thin -> miss
|
||||
vb.open_positions(st, [cand("A")], budget=10)
|
||||
assert st["stats"]["misses"] == 1
|
||||
vb.open_positions(st, [cand("A")], budget=10) # immediately again
|
||||
assert st["stats"]["attempts"] == 1, "re-attempted inside cooldown"
|
||||
|
||||
|
||||
@case("settle: win pays full, refund pays 0.5, loss pays 0 — cash exact")
|
||||
def t5():
|
||||
st = vb.load_state()
|
||||
vb.book_asks = lambda tok: [(0.01, 1000)]
|
||||
for t_ in ("W", "R", "L"):
|
||||
vb.open_positions(st, [cand(t_, event=t_)], budget=10)
|
||||
cash_after_open = st["cash"]
|
||||
vecs = {"0xcW": [1.0, 0.0], "0xcR": [0.5, 0.5], "0xcL": [0.0, 1.0]}
|
||||
vb.onchain_payouts = lambda cond, rpc: vecs[cond]
|
||||
vb.settle(st, rpc="stub", budget=10)
|
||||
s = st["stats"]
|
||||
assert (s["wins"], s["refunds"], s["losses"]) == (1, 1, 1)
|
||||
sh = 1.0 / 0.01
|
||||
assert abs(st["cash"] - (cash_after_open + sh*1.0 + sh*0.5)) < 1e-6
|
||||
assert not st["my_pos"]
|
||||
|
||||
|
||||
@case("unresolved market stays open; feed math consistent")
|
||||
def t6():
|
||||
st = vb.load_state()
|
||||
vb.book_asks = lambda tok: [(0.01, 1000)]
|
||||
vb.open_positions(st, [cand("A")], budget=10)
|
||||
vb.onchain_payouts = lambda cond, rpc: None # denominator 0
|
||||
vb.settle(st, rpc="stub", budget=10)
|
||||
assert "A" in st["my_pos"] and st["stats"]["resolved"] == 0
|
||||
feed = vb.write_feed(st)
|
||||
assert feed["open_count"] == 1 and feed["deployed"] == 1.0
|
||||
assert feed["fill_rate"] == 1.0
|
||||
|
||||
|
||||
print()
|
||||
print("FAILURES:", fails or "none")
|
||||
sys.exit(1 if fails else 0)
|
||||
@@ -0,0 +1,9 @@
|
||||
# wwf-valuebot — VALUE silo image. Deliberately separate from the copybot's
|
||||
# fly.Dockerfile: valuebot is pure stdlib, so this never needs to move when
|
||||
# the copybot's SDK pins change (and vice versa).
|
||||
FROM python:3.12-slim
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
COPY value/start.sh /start.sh
|
||||
RUN chmod +x /start.sh
|
||||
CMD ["/bin/bash", "/start.sh"]
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
# Boot for the wwf-valuebot Fly worker (VALUE silo — value/PLAN.md).
|
||||
# Clones main fresh (public read; GITHUB_TOKEN enables state pushes),
|
||||
# verifies the clone against ls-remote (stale-replica guard, same lesson as
|
||||
# the copybot's), then runs the paper bot. No HTTP service on purpose: no
|
||||
# health-check auto-restarts to race a state surgery (README gotcha 15).
|
||||
set -e
|
||||
REPO="github.com/jaxperro/winning-wallet-finder.git"
|
||||
DIR=/tmp/wwf
|
||||
for i in 1 2 3 4; do
|
||||
rm -rf "$DIR"
|
||||
if [ -n "${GITHUB_TOKEN:-}" ]; then
|
||||
git clone -q --depth 1 "https://x-access-token:${GITHUB_TOKEN}@${REPO}" "$DIR"
|
||||
else
|
||||
git clone -q --depth 1 "https://${REPO}" "$DIR"
|
||||
fi
|
||||
HEAD=$(git -C "$DIR" rev-parse HEAD)
|
||||
REMOTE=$(git ls-remote "https://${REPO}" HEAD | cut -f1)
|
||||
if [ "$HEAD" = "$REMOTE" ]; then
|
||||
echo "[clone-guard] clone verified @ ${HEAD:0:10}"
|
||||
break
|
||||
fi
|
||||
echo "[clone-guard] stale clone ($HEAD != $REMOTE) — retry $i"
|
||||
sleep 5
|
||||
done
|
||||
cd "$DIR"
|
||||
git config user.email "valuebot@wwf" && git config user.name "wwf-valuebot"
|
||||
[ -z "${GITHUB_TOKEN:-}" ] && echo "⚠ no GITHUB_TOKEN — feed publishing will fail (paper book not durable across restarts)"
|
||||
exec python3 -u value/valuebot.py
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,397 @@
|
||||
#!/usr/bin/env python3
|
||||
"""VALUE paper bot — systematic sub-2¢ portfolio (value/PLAN.md, strategy V0).
|
||||
|
||||
SILO RULES (user directive 2026-07-17): this file must not import copybot.py
|
||||
or copytrade.py, share no state/feed/webhook/wallet with the copy trader, and
|
||||
touch only value/* paths. The ~60 lines of book/fee/payout helpers are
|
||||
DUPLICATED here on purpose — total blast-radius isolation is worth it.
|
||||
|
||||
The strategy is a law-of-large-numbers portfolio: every active market with an
|
||||
ask ≤ 2¢ is a candidate; stake is flat $1 (the venue minimum — reality, not
|
||||
choice); positions hold to resolution and settle at CHAIN truth (payout
|
||||
vectors — 0.5 refunds are real). The calibration study says such entries
|
||||
resolved ~1.24x their price; the ONE thing history can't say is whether the
|
||||
fills exist, so the fill model is brutally honest (2026-07-16 parity lesson):
|
||||
a candidate with less than $1 of asks inside the protected band is a MISS,
|
||||
never a pretend fill.
|
||||
|
||||
Run: python3 value/valuebot.py --once # one scan cycle, no publishing
|
||||
python3 value/valuebot.py # loop (Fly worker; publishes feed)
|
||||
"""
|
||||
import argparse
|
||||
import calendar
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import ssl
|
||||
import subprocess
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
REPO = os.path.dirname(HERE)
|
||||
SSL_CTX = ssl._create_unverified_context()
|
||||
|
||||
GAMMA = "https://gamma-api.polymarket.com"
|
||||
CLOB = "https://clob.polymarket.com"
|
||||
CTF = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"
|
||||
_SEL_DEN = "0xdd34de67" # payoutDenominator(bytes32)
|
||||
_SEL_NUM = "0x0504c814" # payoutNumerators(bytes32,uint256)
|
||||
|
||||
STATE = os.path.join(HERE, "valuebot_state.json")
|
||||
FEED = os.path.join(HERE, "valuebot.json")
|
||||
FILLS = os.path.join(HERE, "valuebot_fills.jsonl")
|
||||
|
||||
BANK = 1000.0 # paper bankroll
|
||||
STAKE = 1.0 # flat, = venue minimum (reality)
|
||||
MAX_PX = 0.02 # the studied bucket boundary
|
||||
BAND = 1.05 # protected band: ask*(1+5%), like the live executor
|
||||
MAX_OPEN = 300 # portfolio cap -> max $300 deployed
|
||||
SCAN_S = 300
|
||||
BOOK_BUDGET = 60 # CLOB book fetches per cycle (be a good citizen)
|
||||
SETTLE_BUDGET = 40 # payout-vector checks per cycle
|
||||
COOLDOWN_S = 6 * 3600 # re-look at a skipped/missed token after 6h
|
||||
FEED_PUSH_MIN_S = 300
|
||||
MISS_KEEP = 500 # ledger rows kept in state (totals never truncate)
|
||||
|
||||
# Fee Structure V2 rates by category keyword (entry side only — redeem is free)
|
||||
FEE_RATES = [("crypto", 0.07), ("sport", 0.03), ("esport", 0.03),
|
||||
("finance", 0.04), ("politic", 0.04), ("tech", 0.04),
|
||||
("geopolit", 0.0)]
|
||||
FEE_DEFAULT = 0.05
|
||||
|
||||
|
||||
def log(m):
|
||||
print(f"{time.strftime('%H:%M:%S')} {m}", flush=True)
|
||||
|
||||
|
||||
def get_json(url, timeout=15):
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
||||
with urllib.request.urlopen(req, timeout=timeout, context=SSL_CTX) as r:
|
||||
return json.loads(r.read().decode())
|
||||
|
||||
|
||||
def fee_rate(category):
|
||||
c = (category or "").lower()
|
||||
for k, r in FEE_RATES:
|
||||
if k in c:
|
||||
return r
|
||||
return FEE_DEFAULT
|
||||
|
||||
|
||||
def taker_fee(shares, price, rate):
|
||||
return shares * rate * price * (1.0 - price)
|
||||
|
||||
|
||||
def event_key(slug):
|
||||
"""Correlation group: sub-split slugs collapse to their date prefix (the
|
||||
copy book learned this the hard way — one game, six markets)."""
|
||||
m = re.match(r"(.*?\d{4}-\d{2}-\d{2})", slug or "")
|
||||
return m.group(1) if m else (slug or None)
|
||||
|
||||
|
||||
# ── market data ──────────────────────────────────────────────────────────────
|
||||
|
||||
def scan_universe(max_pages=60):
|
||||
"""Active gamma markets with any outcome priced ≤ MAX_PX. Yields candidate
|
||||
dicts. outcomePrices is gamma's own mark — cheap prefilter only; the CLOB
|
||||
book is the truth a fill model is allowed to use."""
|
||||
out, offset = [], 0
|
||||
for _ in range(max_pages):
|
||||
try:
|
||||
# soonest-ending first: the calibration edge concentrates at short
|
||||
# time-to-resolution, so the book budget goes there before the
|
||||
# 2028-politics dust the default page order surfaces
|
||||
page = get_json(f"{GAMMA}/markets?active=true&closed=false"
|
||||
f"&order=endDate&ascending=true"
|
||||
f"&end_date_min={time.strftime('%Y-%m-%d')}"
|
||||
f"&limit=100&offset={offset}")
|
||||
except Exception as e:
|
||||
log(f"gamma page {offset} failed: {str(e)[:60]}")
|
||||
break
|
||||
if not page:
|
||||
break
|
||||
for m in page:
|
||||
try:
|
||||
prices = [float(x) for x in json.loads(m.get("outcomePrices") or "[]")]
|
||||
toks = json.loads(m.get("clobTokenIds") or "[]")
|
||||
outs = json.loads(m.get("outcomes") or "[]")
|
||||
except Exception:
|
||||
continue
|
||||
if len(prices) != len(toks) or not toks:
|
||||
continue
|
||||
for i, px in enumerate(prices):
|
||||
if 0.0 < px <= MAX_PX:
|
||||
ev = (m.get("events") or [{}])[0]
|
||||
out.append({
|
||||
"token": toks[i], "outcome": outs[i] if i < len(outs) else "?",
|
||||
"mark": px, "cond": m.get("conditionId"),
|
||||
"title": m.get("question") or "",
|
||||
"end": m.get("endDate"), "cat": m.get("category")
|
||||
or ev.get("category") or "",
|
||||
"event": event_key(ev.get("slug") or m.get("slug")),
|
||||
"tok_index": i, "n_outcomes": len(toks)})
|
||||
offset += 100
|
||||
if len(page) < 100:
|
||||
break
|
||||
return out
|
||||
|
||||
|
||||
def book_asks(token):
|
||||
"""Ask ladder [(price, size)] cheapest-first, or None on failure."""
|
||||
try:
|
||||
b = get_json(f"{CLOB}/book?token_id={token}", timeout=8)
|
||||
asks = sorted(((float(a["price"]), float(a["size"]))
|
||||
for a in b.get("asks") or []), key=lambda x: x[0])
|
||||
return asks
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def model_fill(asks, stake, max_px, band=BAND):
|
||||
"""Walk the real ask ladder inside min(max_px, best_ask*band); a FAK for
|
||||
`stake` dollars either fully fills inside the band or is an honest MISS
|
||||
(None, reason). Returns (shares, avg_price, None) on fill."""
|
||||
if asks is None:
|
||||
return None, None, "book fetch failed"
|
||||
if not asks:
|
||||
return None, None, "no asks on the book"
|
||||
best = asks[0][0]
|
||||
if best > max_px:
|
||||
return None, None, f"best ask {best:.3f} above {max_px:.2f}"
|
||||
cap = min(max_px, round(best * band, 6))
|
||||
usd, shares = 0.0, 0.0
|
||||
for px, sz in asks:
|
||||
if px > cap:
|
||||
break
|
||||
take_usd = min(stake - usd, px * sz)
|
||||
shares += take_usd / px
|
||||
usd += take_usd
|
||||
if usd >= stake - 1e-9:
|
||||
return shares, usd / shares, None
|
||||
return None, None, (f"only ${usd:.2f} of asks inside the band "
|
||||
f"(cap {cap:.3f}) — FAK no-match")
|
||||
|
||||
|
||||
# ── chain-truth settlement ───────────────────────────────────────────────────
|
||||
|
||||
def _rpc_url():
|
||||
url = os.environ.get("ALCHEMY_RPC_URL")
|
||||
if url:
|
||||
return url
|
||||
try:
|
||||
k = json.load(open(os.path.join(REPO, "config.json"))).get("alchemy_key")
|
||||
return f"https://polygon-mainnet.g.alchemy.com/v2/{k}" if k else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def onchain_payouts(cond, rpc):
|
||||
"""[p0, p1, ...] in the market's token order, or None if unresolved.
|
||||
Denominator 0 = not resolved; [0.5, 0.5] refunds are REAL payouts."""
|
||||
if not (rpc and cond):
|
||||
return None
|
||||
|
||||
def call(data):
|
||||
body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "eth_call",
|
||||
"params": [{"to": CTF, "data": data}, "latest"]}).encode()
|
||||
req = urllib.request.Request(rpc, data=body,
|
||||
headers={"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=15, context=SSL_CTX) as r:
|
||||
return json.loads(r.read())["result"]
|
||||
try:
|
||||
c = cond[2:].rjust(64, "0")
|
||||
den = int(call(_SEL_DEN + c), 16)
|
||||
if not den:
|
||||
return None
|
||||
n = 2
|
||||
return [int(call(_SEL_NUM + c + hex(i)[2:].rjust(64, "0")), 16) / den
|
||||
for i in range(n)]
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
# ── the bot ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def load_state():
|
||||
try:
|
||||
return json.load(open(STATE))
|
||||
except Exception:
|
||||
return {"cash": BANK, "my_pos": {}, "resolved": [], "missed": [],
|
||||
"attempted": {}, "stats": {"attempts": 0, "fills": 0,
|
||||
"misses": 0, "resolved": 0, "wins": 0, "refunds": 0,
|
||||
"losses": 0, "staked": 0.0, "returned": 0.0, "fees": 0.0},
|
||||
"started": int(time.time())}
|
||||
|
||||
|
||||
def save_state(st):
|
||||
json.dump(st, open(STATE, "w"))
|
||||
|
||||
|
||||
def open_positions(st, cands, budget):
|
||||
"""Try to open new $1 tickets, honest-fill-model, event cap 1."""
|
||||
held_events = {p.get("event") for p in st["my_pos"].values() if p.get("event")}
|
||||
now = time.time()
|
||||
checked = 0
|
||||
for c in cands:
|
||||
if checked >= budget or len(st["my_pos"]) >= MAX_OPEN:
|
||||
break
|
||||
tok = c["token"]
|
||||
if tok in st["my_pos"]:
|
||||
continue
|
||||
if now - st["attempted"].get(tok, 0) < COOLDOWN_S:
|
||||
continue
|
||||
if c["event"] and c["event"] in held_events:
|
||||
continue # correlated dust resolves together
|
||||
if st["cash"] < STAKE:
|
||||
log("CAN'T OPEN — cash exhausted (portfolio at size)")
|
||||
break
|
||||
st["attempted"][tok] = now
|
||||
checked += 1
|
||||
st["stats"]["attempts"] += 1
|
||||
shares, px, reason = model_fill(book_asks(tok), STAKE, MAX_PX)
|
||||
if reason:
|
||||
st["stats"]["misses"] += 1
|
||||
st["missed"].append({"ts": int(now), "token": tok, "mark": c["mark"],
|
||||
"title": c["title"][:60], "reason": reason})
|
||||
st["missed"] = st["missed"][-MISS_KEEP:]
|
||||
continue
|
||||
rate = fee_rate(c["cat"])
|
||||
fee = taker_fee(shares, px, rate)
|
||||
st["cash"] -= STAKE + fee
|
||||
st["stats"]["fills"] += 1
|
||||
st["stats"]["staked"] += STAKE
|
||||
st["stats"]["fees"] += fee
|
||||
st["my_pos"][tok] = {"shares": shares, "cost": STAKE, "fee": round(fee, 6),
|
||||
"price": round(px, 6), "cond": c["cond"],
|
||||
"title": c["title"][:80], "outcome": c["outcome"],
|
||||
"event": c["event"], "end": c["end"],
|
||||
"tok_index": c["tok_index"], "opened": int(now)}
|
||||
held_events.add(c["event"])
|
||||
with open(FILLS, "a") as fh:
|
||||
fh.write(json.dumps({"ts": int(now), "side": "BUY", "token": tok,
|
||||
"shares": round(shares, 4), "price": round(px, 6),
|
||||
"fee": round(fee, 6), "title": c["title"][:60]}) + "\n")
|
||||
log(f"OPEN {shares:,.0f} sh @ {px:.4f} (${STAKE}) · {c['title'][:50]}")
|
||||
# prune the cooldown map so state can't grow unbounded
|
||||
st["attempted"] = {t: ts for t, ts in st["attempted"].items()
|
||||
if now - ts < 2 * COOLDOWN_S}
|
||||
|
||||
|
||||
def settle(st, rpc, budget):
|
||||
"""Chain-truth settlement for positions past their end date."""
|
||||
now = time.time()
|
||||
done = 0
|
||||
for tok, p in list(st["my_pos"].items()):
|
||||
if done >= budget:
|
||||
break
|
||||
end = p.get("end")
|
||||
try:
|
||||
# gamma endDate is UTC — timegm, NOT mktime (repo lesson: mktime
|
||||
# assumes local and shifts settles by the box's UTC offset)
|
||||
end_ts = calendar.timegm(time.strptime(end[:19], "%Y-%m-%dT%H:%M:%S")) if end else 0
|
||||
except Exception:
|
||||
end_ts = 0
|
||||
if end_ts and now < end_ts - 300:
|
||||
continue # not due yet
|
||||
vec = onchain_payouts(p["cond"], rpc)
|
||||
done += 1
|
||||
if vec is None:
|
||||
continue # unresolved — try next cycle
|
||||
idx = min(p.get("tok_index", 0), len(vec) - 1)
|
||||
payout = vec[idx] * p["shares"]
|
||||
st["cash"] += payout
|
||||
s = st["stats"]
|
||||
s["resolved"] += 1
|
||||
s["returned"] += payout
|
||||
kind = ("refund" if 0 < vec[idx] < 1 else
|
||||
"win" if vec[idx] >= 1 else "loss")
|
||||
s["wins" if kind == "win" else "refunds" if kind == "refund" else "losses"] += 1
|
||||
st["resolved"].append({"ts": int(now), "token": tok, "price": p["price"],
|
||||
"cost": p["cost"], "payout": round(payout, 4),
|
||||
"kind": kind, "title": p["title"][:60]})
|
||||
st["resolved"] = st["resolved"][-MISS_KEEP:]
|
||||
del st["my_pos"][tok]
|
||||
log(f"SETTLE {kind.upper()} {payout:+.2f} · entered {p['price']:.4f} · "
|
||||
f"{p['title'][:50]}")
|
||||
|
||||
|
||||
def write_feed(st):
|
||||
s = st["stats"]
|
||||
deployed = sum(p["cost"] for p in st["my_pos"].values())
|
||||
mult = (s["returned"] / s["staked"]) if s["staked"] else None
|
||||
# break-even multiple is 1 + fee drag; the study's promise was ~1.24x
|
||||
feed = {"mode": "paper-value", "bank": BANK, "cash": round(st["cash"], 2),
|
||||
"deployed": round(deployed, 2), "open_count": len(st["my_pos"]),
|
||||
"stats": s, "realized_multiple": round(mult, 4) if mult else None,
|
||||
"fill_rate": round(s["fills"] / s["attempts"], 4) if s["attempts"] else None,
|
||||
"recent_resolved": st["resolved"][-40:], "recent_missed": st["missed"][-40:],
|
||||
"open": [{"t": p["title"], "px": p["price"], "out": p["outcome"],
|
||||
"end": p.get("end")} for p in list(st["my_pos"].values())[:60]],
|
||||
"updated": int(time.time())}
|
||||
json.dump(feed, open(FEED, "w"))
|
||||
return feed
|
||||
|
||||
|
||||
def publish(last_push):
|
||||
"""Commit value/* only. Same pull-rebase-push discipline as the books."""
|
||||
if time.time() - last_push < FEED_PUSH_MIN_S:
|
||||
return last_push
|
||||
try:
|
||||
subprocess.run(["git", "add", "value/valuebot_state.json",
|
||||
"value/valuebot.json", "value/valuebot_fills.jsonl"],
|
||||
cwd=REPO, check=True, capture_output=True)
|
||||
r = subprocess.run(["git", "diff", "--cached", "--quiet"], cwd=REPO)
|
||||
if r.returncode == 0:
|
||||
return last_push
|
||||
subprocess.run(["git", "commit", "-q", "-m",
|
||||
"valuebot: paper feed [skip ci]"], cwd=REPO, check=True,
|
||||
capture_output=True)
|
||||
subprocess.run(["git", "pull", "--rebase", "--autostash", "-q"],
|
||||
cwd=REPO, capture_output=True)
|
||||
subprocess.run(["git", "push", "-q"], cwd=REPO, check=True,
|
||||
capture_output=True, timeout=60)
|
||||
return time.time()
|
||||
except Exception as e:
|
||||
log(f"publish failed (non-fatal): {str(e)[:70]}")
|
||||
return last_push
|
||||
|
||||
|
||||
def cycle(st, rpc, publish_feed=False, last_push=0.0):
|
||||
cands = scan_universe()
|
||||
log(f"universe: {len(cands)} sub-{MAX_PX:.0%} candidates")
|
||||
settle(st, rpc, SETTLE_BUDGET)
|
||||
open_positions(st, cands, BOOK_BUDGET)
|
||||
save_state(st)
|
||||
feed = write_feed(st)
|
||||
s = st["stats"]
|
||||
log(f"book: cash ${st['cash']:,.2f} · open {len(st['my_pos'])} · "
|
||||
f"fills {s['fills']}/{s['attempts']} · resolved {s['resolved']} "
|
||||
f"({s['wins']}W/{s['losses']}L/{s['refunds']}R) · "
|
||||
f"multiple {feed['realized_multiple']}")
|
||||
if publish_feed:
|
||||
last_push = publish(last_push)
|
||||
return last_push
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--once", action="store_true", help="one cycle, no publish")
|
||||
args = ap.parse_args()
|
||||
rpc = _rpc_url()
|
||||
log(f"valuebot · paper · chain settle {'ON' if rpc else 'OFF (no RPC!)'}")
|
||||
st = load_state()
|
||||
if args.once:
|
||||
cycle(st, rpc, publish_feed=False)
|
||||
return
|
||||
last_push = 0.0
|
||||
while True:
|
||||
try:
|
||||
last_push = cycle(st, rpc, publish_feed=True, last_push=last_push)
|
||||
except Exception as e:
|
||||
log(f"cycle error: {str(e)[:100]}")
|
||||
time.sleep(SCAN_S)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,55 @@
|
||||
{"ts": 1784257536, "side": "BUY", "token": "15540133404064485946536607974212890170021691204987131841181394872998839987451", "shares": 111.1111, "price": 0.009, "fee": 0.04955, "title": "Will Harvey Weinstein be sentenced to between 10 and 20 year"}
|
||||
{"ts": 1784257536, "side": "BUY", "token": "90227223115596293448966158151606153337847900483048338623540402385929720677904", "shares": 71.4286, "price": 0.014, "fee": 0.0493, "title": "Will Wes Moore win the 2028 Democratic presidential nominati"}
|
||||
{"ts": 1784257536, "side": "BUY", "token": "35272507273162958827997790327967872292415567859334925862043618606081750011897", "shares": 111.1111, "price": 0.009, "fee": 0.04955, "title": "Will Wes Moore win the 2028 US Presidential Election?"}
|
||||
{"ts": 1784257536, "side": "BUY", "token": "3039641309958397001906153616677074061284510636204155275446291716739429262374", "shares": 62.5, "price": 0.016, "fee": 0.0492, "title": "Will Donald Trump win the 2028 Republican presidential nomin"}
|
||||
{"ts": 1784257536, "side": "BUY", "token": "70997927349469817841862065582625658840347600365813612622959588796331622340305", "shares": 55.5556, "price": 0.018, "fee": 0.0491, "title": "2026 Balance of Power: D Senate, R House"}
|
||||
{"ts": 1784257536, "side": "BUY", "token": "40774280038971372724174457035121000748756395696333205645831548131363721049199", "shares": 1000.0, "price": 0.001, "fee": 0.04995, "title": "Will Sung-Jae Im win the 2026 Masters tournament?"}
|
||||
{"ts": 1784257536, "side": "BUY", "token": "28227838360096074788232992585368970461718254845788738931649369286727917170476", "shares": 1000.0, "price": 0.001, "fee": 0.04995, "title": "Will Mallory McMorrow win the 2026 Michigan Democratic Prima"}
|
||||
{"ts": 1784257536, "side": "BUY", "token": "52634616068523389389514492087655237014427439869589807217055529923225131895030", "shares": 500.0, "price": 0.002, "fee": 0.0499, "title": "Will Tarcisio de Freitas win the 2026 Brazilian presidential"}
|
||||
{"ts": 1784257536, "side": "BUY", "token": "46016408901619903295207375832539898750164493226855442884509133805980187874445", "shares": 52.6316, "price": 0.019, "fee": 0.04905, "title": "Will Tarcisio de Frietas qualify for Brazil's presidential r"}
|
||||
{"ts": 1784257536, "side": "BUY", "token": "28452090262595806306683841543702974671789939255165753137157118490078999198778", "shares": 333.3333, "price": 0.003, "fee": 0.04985, "title": "Will Erling Haaland win the 2026 Ballon d'Or?"}
|
||||
{"ts": 1784257536, "side": "BUY", "token": "95835705062472305459663741402900736177330733688944910993896545106260819635212", "shares": 83.3333, "price": 0.012, "fee": 0.0494, "title": "Will 3 Fed rate cuts happen in 2026?"}
|
||||
{"ts": 1784257536, "side": "BUY", "token": "97079880955117270083666806029014143818118511996487378321036003805764596740938", "shares": 1000.0, "price": 0.001, "fee": 0.04995, "title": "Will Rick Caruso win the California Governor Election in 202"}
|
||||
{"ts": 1784257536, "side": "BUY", "token": "55269299240433422758148290868724152324629587438760275683104577693660207425448", "shares": 111.1111, "price": 0.009, "fee": 0.04955, "title": "Will the Democrats win the Nebraska Senate race in 2026?"}
|
||||
{"ts": 1784257536, "side": "BUY", "token": "112101678667388918252758604047145833695065509511438805706635695881082938013669", "shares": 250.0, "price": 0.004, "fee": 0.0498, "title": "Will Adam Crum win the 2026 Alaska governor election?"}
|
||||
{"ts": 1784257536, "side": "BUY", "token": "2514203837854735205913781359327052377275050226163878960745268210971566855346", "shares": 55.5556, "price": 0.018, "fee": 0.0491, "title": "Will Julian Assange win the Nobel Peace Prize in 2026?"}
|
||||
{"ts": 1784257536, "side": "BUY", "token": "58858731796442679222989272055454043286056057669744610936854497026401512278651", "shares": 52.6316, "price": 0.019, "fee": 0.04905, "title": "Will Trump sell 10k-25k Gold Cards in 2026?"}
|
||||
{"ts": 1784257536, "side": "BUY", "token": "2638212763635765664638931184596338750226895714451963915771516616454156509244", "shares": 58.8235, "price": 0.017, "fee": 0.04915, "title": "Fed abolished before 2027?"}
|
||||
{"ts": 1784257536, "side": "BUY", "token": "26880262286353559461109525615512763357587866035194597494489295218957286731245", "shares": 58.8235, "price": 0.017, "fee": 0.04915, "title": "Will Zelenskyy and Putin meet next in Turkey before 2027?"}
|
||||
{"ts": 1784257536, "side": "BUY", "token": "391670908629060088409431896998377161667071768186645889213963384874929102170", "shares": 166.6667, "price": 0.006, "fee": 0.0497, "title": "Will Microsoft be the largest company in the world by market"}
|
||||
{"ts": 1784257536, "side": "BUY", "token": "50627054342292524063405225334567943340537585485453009001079628591959212607784", "shares": 250.0, "price": 0.004, "fee": 0.0498, "title": "Will Sergey Brin be richest person on December 31?"}
|
||||
{"ts": 1784257536, "side": "BUY", "token": "3778701297142172054009835709694047837281608259621414725374943993931338899258", "shares": 100.0, "price": 0.01, "fee": 0.0495, "title": "Will US GDP growth in 2026 be between 1.0% and 1.5%?"}
|
||||
{"ts": 1784257536, "side": "BUY", "token": "81662326158871781857247725348568394697379926716334270967994039975048021832777", "shares": 500.0, "price": 0.002, "fee": 0.0499, "title": "Will Wicked: For Good be the top grossing movie of 2026?"}
|
||||
{"ts": 1784257536, "side": "BUY", "token": "78811978781022187641776289375780683316166629912443452648985718823109953148781", "shares": 50.0, "price": 0.02, "fee": 0.049, "title": "Will The Odyssey have the best domestic opening weekend in 2"}
|
||||
{"ts": 1784257536, "side": "BUY", "token": "36385708241103656881636928091267439785598872725658076323572114247897288109684", "shares": 333.3333, "price": 0.003, "fee": 0.04985, "title": "Will 2026 be the fifth-hottest year on record?"}
|
||||
{"ts": 1784257536, "side": "BUY", "token": "103149043231796752807018971660605675992799889771393087586255256574936406270971", "shares": 50.0, "price": 0.02, "fee": 0.049, "title": "Will Israel strike 7 countries in 2026?"}
|
||||
{"ts": 1784257674, "side": "BUY", "token": "67270379508707453579822352759295200887632344396882723020536262528886991089078", "shares": 1000.0, "price": 0.001, "fee": 0.04995, "title": "Will SpaceX Starship Flight Test 13 launch by July 16?"}
|
||||
{"ts": 1784257674, "side": "BUY", "token": "112530613306716166624306785625527243000936556874970549731102935274624880614808", "shares": 1000.0, "price": 0.001, "fee": 0.04995, "title": "Will the highest temperature in Austin be 75\u00b0F or below on J"}
|
||||
{"ts": 1784257674, "side": "BUY", "token": "670318695093788286770480440596440124473469459020377953294919928322360769304", "shares": 1000.0, "price": 0.001, "fee": 0.04995, "title": "Will the lowest temperature in Miami be 90\u00b0F or higher on Ju"}
|
||||
{"ts": 1784257674, "side": "BUY", "token": "41890243833972194654560034930220120500393044272585435142490430815390166569328", "shares": 1000.0, "price": 0.001, "fee": 0.04995, "title": "Will the highest temperature in Denver be between 98-99\u00b0F on"}
|
||||
{"ts": 1784257674, "side": "BUY", "token": "95737904431779569676576297188097388550423295707795662504699790788164073261113", "shares": 1000.0, "price": 0.001, "fee": 0.04995, "title": "Will the highest temperature in Seattle be between 68-69\u00b0F o"}
|
||||
{"ts": 1784257674, "side": "BUY", "token": "108827586676863634986160975066769755770958032278021173591928361262685472235476", "shares": 1000.0, "price": 0.001, "fee": 0.04995, "title": "Will the highest temperature in Miami be between 102-103\u00b0F o"}
|
||||
{"ts": 1784257674, "side": "BUY", "token": "40484969606304977313516258463960677557112896676385917551257832992338373395909", "shares": 1000.0, "price": 0.001, "fee": 0.04995, "title": "Will the highest temperature in Panama City be 32\u00b0C on July "}
|
||||
{"ts": 1784257674, "side": "BUY", "token": "84776563282316625801986456207300733163917012868680022547211735104037218203714", "shares": 1000.0, "price": 0.001, "fee": 0.04995, "title": "Will the highest temperature in San Francisco be between 68-"}
|
||||
{"ts": 1784257674, "side": "BUY", "token": "29347276364974731963385024309120732757112983303839511905461040930236071572317", "shares": 1000.0, "price": 0.001, "fee": 0.04995, "title": "Will the lowest temperature in New York City be between 72-7"}
|
||||
{"ts": 1784257674, "side": "BUY", "token": "51262374926903001932468671028221713571892463560945941801282501273513087284674", "shares": 1000.0, "price": 0.001, "fee": 0.04995, "title": "Will the highest temperature in New York City be 87\u00b0F or bel"}
|
||||
{"ts": 1784257674, "side": "BUY", "token": "25239250531321775957539227586259572323063933935163514081809767561933575671111", "shares": 1000.0, "price": 0.001, "fee": 0.04995, "title": "Will the highest temperature in Buenos Aires be 17\u00b0C on July"}
|
||||
{"ts": 1784257674, "side": "BUY", "token": "22754187563426351457864708727605642757046404804856337280235128783120898225547", "shares": 1000.0, "price": 0.001, "fee": 0.04995, "title": "Will the highest temperature in Dallas be between 94-95\u00b0F on"}
|
||||
{"ts": 1784257674, "side": "BUY", "token": "60618958870658530867242758388795004561381687504211666692788906933769701638415", "shares": 1000.0, "price": 0.001, "fee": 0.04995, "title": "Will the highest temperature in Houston be between 94-95\u00b0F o"}
|
||||
{"ts": 1784257674, "side": "BUY", "token": "60580767462908003202521160373814734051541521270964650920813469375623674696956", "shares": 1000.0, "price": 0.001, "fee": 0.04995, "title": "Will the highest temperature in Toronto be 36\u00b0C on July 16?"}
|
||||
{"ts": 1784257674, "side": "BUY", "token": "29856550469832073692064803847822653016342885271929267791592525678769760700642", "shares": 1000.0, "price": 0.001, "fee": 0.04995, "title": "Will the highest temperature in Atlanta be between 96-97\u00b0F o"}
|
||||
{"ts": 1784257674, "side": "BUY", "token": "51846560519512929162317725669327948765820777581623655178503251026299837013346", "shares": 1000.0, "price": 0.001, "fee": 0.04995, "title": "Will the highest temperature in Chicago be 108\u00b0F or higher o"}
|
||||
{"ts": 1784257674, "side": "BUY", "token": "16892108417177785233910824250110050108374072497266057215023603815415832076961", "shares": 1000.0, "price": 0.001, "fee": 0.04995, "title": "Will the highest temperature in Mexico City be 21\u00b0C on July "}
|
||||
{"ts": 1784257674, "side": "BUY", "token": "113304036176992406252151057788661842368048044822407179185609010385077607446716", "shares": 1000.0, "price": 0.001, "fee": 0.04995, "title": "Will the highest temperature in Los Angeles be between 82-83"}
|
||||
{"ts": 1784257674, "side": "BUY", "token": "114906998005567740062379006506757539789607609377950129848657565529321642457520", "shares": 1000.0, "price": 0.001, "fee": 0.04995, "title": "Will the highest temperature in Sao Paulo be 28\u00b0C on July 16"}
|
||||
{"ts": 1784257674, "side": "BUY", "token": "58017463736482610065171386990548333051004054628869782709973003023159082007681", "shares": 1000.0, "price": 0.001, "fee": 0.04995, "title": "Botafogo FR vs. Santos FC: Both Teams to Score in Second Hal"}
|
||||
{"ts": 1784257674, "side": "BUY", "token": "15390009074522870667148660165062730555236796460163940026652310740380869259440", "shares": 1000.0, "price": 0.001, "fee": 0.04995, "title": "Will CR Brasil vs. Clube N\u00e1utico Capibaribe end in a draw?"}
|
||||
{"ts": 1784257674, "side": "BUY", "token": "6390292418477923671015324158056070535023707144836057612462312618380536361710", "shares": 125.0, "price": 0.008, "fee": 0.0496, "title": "Will Trump say \"Crooked\" during Speech to the Nation?"}
|
||||
{"ts": 1784257674, "side": "BUY", "token": "27992248566285722250207451571631985433667405172488328447455776588169729238911", "shares": 50.0, "price": 0.02, "fee": 0.049, "title": "Will Lyric Medeiros be evicted in Big Brother season 28 (Wee"}
|
||||
{"ts": 1784257674, "side": "BUY", "token": "90402212796201292820992439163974818243468756690479945100638704540510089666133", "shares": 111.1111, "price": 0.009, "fee": 0.04955, "title": "Will Christopher John Brown win the 2026 Norfolk Police and "}
|
||||
{"ts": 1784257674, "side": "BUY", "token": "22779532518265431207930623944582312092826054206590222759621637519175542424824", "shares": 111.1111, "price": 0.009, "fee": 0.04955, "title": "Spread: Cangrejeros de Santurce (-3.5)"}
|
||||
{"ts": 1784257674, "side": "BUY", "token": "41920127678793154931001634763019653359905479407099008135587603675879855203603", "shares": 111.1111, "price": 0.009, "fee": 0.04955, "title": "Spread: Leones de Ponce (-9.5)"}
|
||||
{"ts": 1784257674, "side": "BUY", "token": "102623997170607052328502977572291446032357640641101995688679962871454255972010", "shares": 50.0, "price": 0.02, "fee": 0.049, "title": "Exact Score: Chicago Fire FC 0 - 3 Vancouver Whitecaps FC?"}
|
||||
{"ts": 1784257674, "side": "BUY", "token": "9346922233582986919029525981694074220837187281159717245848028631630516501435", "shares": 1000.0, "price": 0.001, "fee": 0.04995, "title": "St. Louis City SC to score first vs. Sporting Kansas City?"}
|
||||
{"ts": 1784257674, "side": "BUY", "token": "105174454143504295787911892416971339275463065080906818567252962655061970238332", "shares": 100.0, "price": 0.01, "fee": 0.0495, "title": "Map 1 Total Rounds: Over/Under 24.5"}
|
||||
{"ts": 1784257674, "side": "BUY", "token": "39427388354114737168139623919863330297267487065887316018128781936470792293870", "shares": 1000.0, "price": 0.001, "fee": 0.04995, "title": "Club Necaxa vs. Atlante FC: Club Necaxa O/U 0.5"}
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user