Add cloud state sync

This commit is contained in:
Theodore Song
2026-07-05 22:30:26 -04:00
parent f98ec33f04
commit 7168779bcb
5 changed files with 442 additions and 9 deletions
+29 -9
View File
@@ -381,9 +381,9 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
</div>
<div class="card">
<div class="card-h"><h3>Device sync</h3></div>
<p class="muted" style="margin:0">Portfolios, returns, reports, suggestions, and chart history are saved in this browser. To make another device match, click <b>Copy Sync</b> here, then open the site on the other device and use <b>Import Sync</b>.</p>
<p class="muted" style="margin:0">Portfolios, returns, reports, suggestions, and chart history now sync through the site automatically. <b>Copy Sync</b> and <b>Import Sync</b> remain as a fallback for moving the exact state between devices.</p>
</div>
<div class="disclaimer">⚠️ <b>Paper trading only — not financial advice.</b> Nothing here places real orders or moves money. The analysis is a transparent heuristic. All portfolios live in this browser's local storage (each device keeps its own).</div>
<div class="disclaimer">⚠️ <b>Paper trading only — not financial advice.</b> Nothing here places real orders or moves money. The analysis is a transparent heuristic. Portfolios sync through the site when cloud state is available, with this browser keeping a local backup.</div>
</section>
<footer>
@@ -397,7 +397,7 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
/* ============================================================
Polymarket Arena — fully client-side.
Fetches live markets, scores them, and runs ten competing
paper-trading agents. State persists in localStorage.
paper-trading agents. State syncs through /api/state when configured and uses localStorage as the on-device cache.
============================================================ */
const GAMMA = "https://gamma-api.polymarket.com";
@@ -582,6 +582,24 @@ function loadState(){
function saveState(st){localStorage.setItem(AGENTS_KEY,JSON.stringify(st));}
function loadSuggestions(){try{const s=localStorage.getItem(SUG_KEY);return s?JSON.parse(s):{date:null,suggestions:[]};}catch(e){return {date:null,suggestions:[]};}}
function saveSuggestions(sugs){const p={date:todayStr(),generated_at:nowIso(),suggestions:sugs};localStorage.setItem(SUG_KEY,JSON.stringify(p));return p;}
function collectSyncItems(){const items={};SYNC_KEYS.forEach(k=>{const v=localStorage.getItem(k);if(v!=null)items[k]=v;});return items;}
function applySyncItems(items){if(!items||typeof items!=="object")return false;SYNC_KEYS.forEach(k=>{if(items[k]!=null)localStorage.setItem(k,items[k]);});return true;}
async function pushCloudState(){
try{
const r=await fetch("/api/state",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({items:collectSyncItems()})});
if(!r.ok)throw new Error("cloud sync unavailable");
return true;
}catch(e){return false;}
}
async function pullCloudState(){
try{
const r=await fetch("/api/state",{cache:"no-store"});
if(!r.ok)return false;
const d=await r.json();
if(d&&d.state&&applySyncItems(d.state.items)){toast("Cloud state loaded for this device.");return true;}
}catch(e){}
return false;
}
/* ---------- Agent mechanics ---------- */
const equity=(p)=>p.cash+p.positions.reduce((s,x)=>s+x.shares*x.current_price,0);
@@ -995,6 +1013,7 @@ async function runDailyCycle(){
}
st.date=todayStr();st.last_run=SNAP_TS;
saveState(st);
await pushCloudState();
setStatus("up to date",false);
return {suggestions:sugs.length};
}finally{SNAP_TS=null;}
@@ -1333,8 +1352,7 @@ $("runBtn").addEventListener("click",async()=>{
const b64Encode=(s)=>btoa(Array.from(new TextEncoder().encode(s),b=>String.fromCharCode(b)).join(""));
const b64Decode=(s)=>new TextDecoder().decode(Uint8Array.from(atob(s),c=>c.charCodeAt(0)));
function buildSyncCode(){
const data={version:1,exported_at:nowIso(),items:{}};
SYNC_KEYS.forEach(k=>{const v=localStorage.getItem(k);if(v!=null)data.items[k]=v;});
const data={version:1,exported_at:nowIso(),items:collectSyncItems()};
return "PMA1."+b64Encode(JSON.stringify(data));
}
async function readClipboardOrPrompt(){
@@ -1354,25 +1372,27 @@ $("importStateBtn").addEventListener("click",async()=>{
const payload=JSON.parse(b64Decode(code.replace(/^PMA1\./,"")));
if(!payload||payload.version!==1||!payload.items)throw new Error("Bad sync code");
if(!confirm("Import this sync state and replace this device's current local portfolio data?"))return;
SYNC_KEYS.forEach(k=>{if(payload.items[k]!=null)localStorage.setItem(k,payload.items[k]);});
applySyncItems(payload.items);
await pushCloudState();
toast("Sync imported. This device now matches the copied state.");
renderAll();showTab(location.hash.slice(1)||"portfolio");
}catch(e){toast("Import failed. Check that the sync code was copied fully.");}
});
$("resetBtn").addEventListener("click",()=>{
if(!confirm("Reset all ten agents to their $10,000 starting balance?"))return;
saveState(defaultState());localStorage.removeItem(SUG_KEY);toast("All agents reset.");renderAll();
saveState(defaultState());localStorage.removeItem(SUG_KEY);pushCloudState();toast("All agents reset.");renderAll();
});
/* On load: first visit runs a 7-day backtest; afterwards a live daily update. */
(async function init(){
showTab(location.hash.slice(1)||"overview");
const cloudLoaded=await pullCloudState();
renderAll();
const st=loadState();
const btn=$("runBtn");
if(!st.seeded){
btn.disabled=true;btn.textContent="Backtesting…";
try{await backtestWeek(st);st.date=todayStr();st.last_run=nowIso();saveState(st);renderAll();
try{await backtestWeek(st);st.date=todayStr();st.last_run=nowIso();saveState(st);await pushCloudState();renderAll();
toast("7-day backtest complete — live tracking begins today.");}
catch(e){setStatus("backtest error — click Run",false);}
btn.disabled=false;btn.textContent="Run cycle";
@@ -1381,7 +1401,7 @@ $("resetBtn").addEventListener("click",()=>{
try{await runDailyCycle();renderAll();toast("Daily update ready.");}
catch(e){setStatus("error — click Run to retry",false);}
btn.disabled=false;btn.textContent="Run cycle";
}else{setStatus("up to date",false);}
}else{setStatus("up to date",false);if(!cloudLoaded)pushCloudState();}
})();
</script>
</body>