cache schema v2: token-keyed upsert archive, provenance, raw prices, honest failures

- bets table gains asset (token id = position identity), src/ts (endpoint
  provenance + close time), resolved (False = early-sold in an unended market;
  won is a curPrice mark). Auto-migrates v1 in place (~8s, 3,697 exact dupes
  merged); legacy rows carry NULLs until their wallet refreshes.
- refresh is now an upsert by token instead of a wallet wipe: rows sliding out
  of the rolling pull window survive, so per-wallet history accumulates into a
  permanent archive. Same-asset rows from both endpoints (partially-closed
  positions) dedupe to the larger-stake row - kills the two-endpoint
  double-count class (~35k suspect pairs found in the audit).
- p stored raw (0 = avgPrice missing), clamped on read by get_bets, so missing
  prices stay distinguishable from real 0.1c longshots; insider CLI + oos clamp
  their own direct use.
- resolved_bets(strict=True): a failed page raises instead of returning a
  silently truncated history; get_bets no longer caches or marks failed pulls
  (pre-v2 an API error cached the wallet as empty-and-fresh for 14 days -
  Kruto2027 was a live victim of this last night).

Verified: migration 18,289,320 -> 18,285,623 rows; forced refreshes of two
sharps show 0 same-asset dups, 0 legacy/new mixing, clamped reads, conviction
stats intact (Kruto conv win 73%).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jaxperro
2026-07-02 09:26:41 -04:00
parent 29a4bcca9e
commit cc44667b7e
4 changed files with 144 additions and 28 deletions
+28 -3
View File
@@ -47,8 +47,22 @@ def _parse_end(end):
return 0
def resolved_bets(wallet, cutoff, max_pages=40):
"""Resolved bets with entry price, conditionId, resolution time, size."""
def resolved_bets(wallet, cutoff, max_pages=40, strict=False):
"""Resolved bets with entry price, conditionId, token (asset), resolution
time, size, and provenance (cache schema v2).
* ``p`` is the RAW avgPrice (0 when the API omits it) — callers clamp for
the z math; storing raw keeps "missing price" distinguishable from a real
0.1¢ longshot in the cache.
* ``asset`` (token id) is the position identity — it disambiguates the
two-endpoint union (same asset in /closed-positions and /positions is ONE
position seen twice, not two bets) and YES/NO both-sides holdings.
* ``resolved`` is False for early-sold positions in markets that had not
ended at pull time — their ``won`` is a curPrice mark, not an outcome.
* ``strict``: raise on a failed page pull instead of returning a silently
truncated history — a partial pull must never be cached as a wallet's
complete record.
"""
now = time.time()
out = []
for endpoint in ("/closed-positions", "/positions"):
@@ -60,6 +74,8 @@ def resolved_bets(wallet, cutoff, max_pages=40):
else:
params["sizeThreshold"] = 0.0
page = sm.get_json(endpoint, params)
if page is None and strict:
raise RuntimeError(f"{endpoint} pull failed for {wallet} at offset {off}")
if not page:
break
for p in page:
@@ -69,17 +85,24 @@ def resolved_bets(wallet, cutoff, max_pages=40):
if ts < cutoff:
continue
res_t = end or ts
resolved = bool(end) and end <= now
else:
ts = None
if not (cutoff <= end < now):
continue
res_t = end
resolved = True
out.append({
"won": p.get("curPrice", 0) >= 0.5,
"p": max(0.001, min(0.999, p.get("avgPrice", 0) or 0)),
"p": p.get("avgPrice", 0) or 0, # raw — callers clamp
"cond": p.get("conditionId"),
"asset": p.get("asset"),
"res_t": res_t,
"size": p.get("initialValue") or
(p.get("avgPrice", 0) * p.get("totalBought", 0)),
"src": "closed" if endpoint == "/closed-positions" else "open",
"ts": ts,
"resolved": resolved,
})
off += 50
if len(page) < 50:
@@ -123,6 +146,8 @@ def analyze(cand):
bets = resolved_bets(wallet, cutoff)
if len(bets) < 15:
return None
for b in bets: # v2 returns raw p — clamp for the z math
b["p"] = max(0.001, min(0.999, b["p"] or 0))
first_buy, _ = entry_times(wallet)
total_trades = (sm.get_json("/traded", {"user": wallet}) or {}).get("traded", 0)