
    i1I                       d Z ddlmZ ddlZddlZddlZddlZddlmZmZ ddl	m
Z
 ddlZddlmZ ej                  j                  ej                  j                  ej                  j!                  e                  Zeej                  vrej                  j'                  de       ddlmZ d&dZdd		 	 	 	 	 	 	 	 	 	 	 	 	 d'dZd(dZ ej2                          ej4                  ddd       ej4                  ddd ej                  de
      d       ej4                  dd
d ej                  de
      d       ej4                  ddded       ej4                  dd ej8                  g d      d        ej4                  d!d"ded#      	 	 	 	 	 	 	 	 	 	 	 	 	 	 d)d$                                                 Zed%k(  r e        yy)*z
Export an event-driven orderbook replay viewer for a recorded LPL/LOL market.

This viewer is built from orderbook_events.jsonl, not periodic snapshots. It is
intended for microstructure analysis around video-marked game events.
    )annotationsN)datetimetimezone)Path)logger)iter_event_framesc                    t        j                  | dd      j                  dd      j                  dd      j                  dd	      S )
NF),:)ensure_ascii
separatorsz</z<\/u    z\u2028u    z\u2029)jsondumpsreplace)values    )scripts/export_lpl_event_replay_viewer.py
_safe_jsonr      s;    

5uD	v		9	%	9	%	    )market_kindoutput_pathc           	        t        | |||      \  }}|dkD  r|d d |   }|| z  }||rd| nd}	|d|	 dz  }|j                  j                  dd       |||||xs dd	d
ddt        j                  t
        j                        j                         d}
|j                  t        |
      d       | t        |      t        |      |r|d   d	   nd|r
|d   d	   dS ddS )N)slug	data_rootdepthr      _ event_orderbook_replayz.htmlT)parentsexist_okreceived_at_wallexchange_tszreceived_at_wall - exchange_ts)primary	secondarydelay)metaframesr   frame_strider   
time_basisgenerated_atzutf-8)encodingr   )r   r'   r   
started_atended_at)r   parentmkdirr   nowr   utc	isoformat
write_text_render_htmllenstr)r   r   r   r   r(   r   r&   r'   
market_dirsuffixpayloads              r   export_event_viewerr;   #   s.    %	LD& a,'T!J&11[M"r %;F85#IITD9$"(b)&5

 !X\\2<<>G <07Cf+;'7=fQi 2326<F2J12 
 CE r   c                    t        |       }t        j                  t        | j	                  d      xs i j	                  d      xs d            }d}|j                  d|      j                  d|      S )Nr&   questionzEvent Replaya+9  <!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>__TITLE__</title>
  <style>
    :root { color-scheme: dark; --bg:#0f1419; --panel:#151b21; --line:#28313a; --muted:#8b98a6; --text:#f1f4f7; --red:#ef4444; --green:#35c978; --accent:#8ab4ff; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
    * { box-sizing: border-box; }
    body { margin: 0; overflow: auto; background: var(--bg); color: var(--text); }
    .app { width: min(1700px, 100vw); min-height: 100vh; margin: 0 auto; padding: 8px 8px 70px; display: grid; grid-template-rows: auto minmax(0, 1fr); gap: 8px; }
    .top { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; align-items: center; min-height: 34px; }
    h1 { margin: 0; font-size: 15px; }
    .meta { color: var(--muted); font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .stats { display: flex; gap: 12px; color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; }
    .main { min-height: 0; display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1fr) 380px; gap: 8px; }
    .team, .event { min-height: 0; display: grid; grid-template-rows: auto auto minmax(150px, 1fr); gap: 8px; }
    .title, .event-title { height: 30px; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 10px; align-items: center; padding: 0 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); font-size: 12px; }
    .title strong { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
    .book, .box { min-height: 0; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: var(--panel); }
    .book { height: 382px; }
    .head, .row, .mid { display: grid; grid-template-columns: 58px 76px 96px 106px; justify-content: end; align-items: center; }
    .head { height: 28px; color: var(--muted); border-bottom: 1px solid var(--line); font-size: 10px; font-weight: 800; text-transform: uppercase; letter-spacing: .7px; }
    .row { position: relative; min-height: 29px; font-size: 12px; border-bottom: 1px solid rgba(255,255,255,.025); }
    .num { text-align: right; padding-right: 8px; font-variant-numeric: tabular-nums; }
    .price { font-size: 14px; font-weight: 800; }
    .ask .price { color: var(--red); }
    .bid .price { color: var(--green); }
    .bar { position: absolute; inset: 0 auto 0 0; opacity: .75; pointer-events: none; }
    .ask .bar { background: rgba(239,68,68,.13); }
    .bid .bar { background: rgba(53,201,120,.13); }
    .mid { min-height: 32px; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); color: var(--muted); font-size: 11px; font-weight: 750; }
    .pill { display:inline-flex; border-radius:5px; padding:4px 7px; color:white; font-size:11px; font-weight:800; }
    .pill.ask { background:#dc2626; }
    .pill.bid { background:#2ca461; }
    .label { padding-left: 8px; }
    .event-body { padding: 10px; overflow: auto; font-size: 12px; }
    .trade-panel { min-height: 170px; max-height: calc(100vh - 500px); }
    .kv { display: grid; grid-template-columns: 112px minmax(0,1fr); gap: 8px; margin: 5px 0; color: var(--muted); }
    .kv strong { color: var(--text); overflow-wrap: anywhere; }
    .changes { margin-top: 10px; border-top: 1px solid var(--line); padding-top: 8px; }
    .trade-list { display: grid; gap: 3px; }
    .change-row, .trade-row { display: grid; grid-template-columns: minmax(0,1fr) 44px 58px 76px; gap: 6px; min-height: 24px; align-items: center; font-variant-numeric: tabular-nums; border-bottom: 1px solid rgba(255,255,255,.025); }
    .trade-row div, .change-row div { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .trade-row.buy .trade-price { color: var(--green); font-weight: 800; }
    .trade-row.sell .trade-price { color: var(--red); font-weight: 800; }
    .controls { position: fixed; left: 8px; right: 8px; bottom: 8px; z-index: 20; width: min(1684px, calc(100vw - 16px)); margin: 0 auto; display: grid; grid-template-columns: 86px 74px 74px 74px 1fr; gap: 8px; align-items: center; background: rgba(21,27,33,.96); border: 1px solid var(--line); border-radius: 8px; padding: 7px; box-shadow: 0 10px 24px rgba(0,0,0,.35); backdrop-filter: blur(8px); }
    button, select { height: 28px; border-radius: 6px; border: 1px solid #34414d; background: #202832; color: var(--text); font-size: 12px; }
    input[type=range] { width: 100%; accent-color: var(--accent); }
  </style>
</head>
<body>
<script id="replay-data" type="application/json">__DATA__</script>
<div class="app">
  <div class="top">
    <div><h1>Event Order Book Replay</h1><div id="question" class="meta"></div></div>
    <div class="stats"><span id="recvTime"></span><span id="frameNo"></span></div>
  </div>
  <div class="main"><div id="teams" class="main" style="display:contents"></div><aside class="event"><div class="event-title"><strong>Event</strong><span id="delay"></span></div><div class="box event-body" id="eventBody"></div></aside></div>
  <div class="controls">
    <select id="speed"><option value="0.5">0.5x</option><option value="1">1x</option><option value="2" selected>2x</option><option value="5">5x</option><option value="10">10x</option></select>
    <button id="play">Play</button>
    <button id="back5">-5s</button>
    <button id="step">Step</button>
    <input id="slider" type="range" min="0" max="0" value="0">
  </div>
</div>
<script>
const payload = JSON.parse(document.getElementById('replay-data').textContent);
const frames = payload.frames || [];
const tokenIds = (payload.meta && payload.meta.token_ids) || [];
let idx = 0;
let playing = false;
let rafId = null;
let lastTick = 0;
let frameCarry = 0;
let historyBuiltTo = -1;
const tradeHistory = new Map();
const els = { question: document.getElementById('question'), teams: document.getElementById('teams'), recvTime: document.getElementById('recvTime'), frameNo: document.getElementById('frameNo'), delay: document.getElementById('delay'), eventBody: document.getElementById('eventBody'), speed: document.getElementById('speed'), play: document.getElementById('play'), back5: document.getElementById('back5'), step: document.getElementById('step'), slider: document.getElementById('slider') };
const teamEls = new Map();
function fmtTime(value) { const d = new Date(value); if (Number.isNaN(d.getTime())) return value || '-'; const parts = new Intl.DateTimeFormat('en-CA', { timeZone:'Asia/Shanghai', hour12:false, month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit', second:'2-digit' }).formatToParts(d).reduce((a,p)=>{a[p.type]=p.value; return a;}, {}); return `${parts.month}-${parts.day} ${parts.hour}:${parts.minute}:${parts.second}.${String(d.getMilliseconds()).padStart(3,'0')}`; }
function cents(v) { return v === null || v === undefined || v === '' ? '-' : `${Math.round(Number(v) * 1000) / 10}c`; }
function shares(v) { return Number(v || 0).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); }
function money(v) { return `$${Number(v || 0).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; }
function row(level, side, max, before) { const n = Number(level.price) * Number(level.size); const width = Math.max(2, Math.min(100, n / Math.max(1, max) * 100)); return `<div class="row ${side}"><div class="bar" style="width:${width}%"></div><div></div><div class="price num">${cents(level.price)}</div><div class="num">${shares(level.size)}</div><div class="num">${money(before + n)}</div></div>`; }
function sideRows(levels, side) { const top = (levels || []).slice(0, 5); const max = Math.max(1, ...top.map(l => Number(l.price) * Number(l.size))); let cum = 0; const rows = top.map(l => { const out = row(l, side, max, cum); cum += Number(l.price) * Number(l.size); return out; }); return (side === 'ask' ? rows.reverse() : rows).join(''); }
function makeTeam(tokenId) { const sample = frames.find(f => f.books && f.books[tokenId]); const outcome = sample ? sample.books[tokenId].outcome : tokenId; const el = document.createElement('section'); el.className = 'team'; el.innerHTML = `<div class="title"><strong>${outcome}</strong><span class="best"></span></div><div class="book"><div class="head"><div></div><div class="num">Price</div><div class="num">Shares</div><div class="num">Total</div></div><div class="asks"></div><div class="mid"><div class="label"><span class="pill ask">Asks</span></div><div class="last num"></div><div class="spread num"></div><div></div></div><div class="bids"></div></div><div class="box event-body trade-panel"><div class="trade"></div></div>`; els.teams.appendChild(el); teamEls.set(tokenId, { best: el.querySelector('.best'), asks: el.querySelector('.asks'), bids: el.querySelector('.bids'), last: el.querySelector('.last'), spread: el.querySelector('.spread'), trade: el.querySelector('.trade') }); }
function resetTradeHistory() { tradeHistory.clear(); for (const t of tokenIds) tradeHistory.set(t, []); historyBuiltTo = -1; }
function appendTrade(frameIndex) { const tr = frames[frameIndex] && frames[frameIndex].trade; if (!tr || !tr.asset_id || !tradeHistory.has(tr.asset_id)) return; const prev = tradeHistory.get(tr.asset_id); const key = `${tr.transaction_hash || ''}|${tr.price}|${tr.size}|${frames[frameIndex].received_at_wall}`; if (!prev.some(x => x.key === key)) prev.push({ ...tr, key, received_at_wall: frames[frameIndex].received_at_wall, exchange_ts: frames[frameIndex].exchange_ts }); }
function buildTradeHistory(frameIndex) { if (frameIndex < historyBuiltTo) resetTradeHistory(); for (let i = historyBuiltTo + 1; i <= frameIndex; i++) appendTrade(i); historyBuiltTo = Math.max(historyBuiltTo, frameIndex); }
function renderTradeList(tokenId, latest) { const rows = (tradeHistory.get(tokenId) || []).slice(-80).reverse(); if (!rows.length) return '<div class="trade-row"><div>No trade yet</div><div></div><div></div><div></div></div>'; return `<div class="trade-list">${rows.map(t => `<div class="trade-row ${String(t.side).toLowerCase()}"><div title="${t.transaction_hash || ''}">${fmtTime(t.received_at_wall)}</div><div>${t.side}</div><div class="trade-price num">${cents(t.price)}</div><div class="num">${shares(t.size)}</div></div>`).join('')}</div>`; }
function renderTeam(tokenId, frame) { const book = frame.books[tokenId]; const view = teamEls.get(tokenId); if (!book || !view) return; const bid = Number(book.best_bid); const ask = Number(book.best_ask); view.best.textContent = `${cents(book.best_bid)} / ${cents(book.best_ask)}`; view.asks.innerHTML = sideRows(book.asks, 'ask'); view.bids.innerHTML = sideRows(book.bids, 'bid'); view.last.textContent = book.last_trade ? `Last ${cents(book.last_trade.price)}` : 'Last -'; view.spread.textContent = Number.isFinite(bid) && Number.isFinite(ask) ? `Spread ${cents(ask - bid)}` : 'Spread -'; view.trade.innerHTML = renderTradeList(tokenId, book.last_trade); }
function renderEvent(frame) { const changes = (frame.changed_levels || []).slice(0, 12).map(c => `<div class="change-row"><div>${(frame.books[c.asset_id] && frame.books[c.asset_id].outcome) || c.asset_id}</div><div>${c.side}</div><div class="num">${cents(c.price)}</div><div class="num">${shares(c.size)}</div></div>`).join(''); const trade = frame.trade ? `<div class="trade-row ${String(frame.trade.side).toLowerCase()}"><div>${frame.trade.outcome}</div><div>${frame.trade.side}</div><div class="trade-price num">${cents(frame.trade.price)}</div><div class="num">${shares(frame.trade.size)}</div></div>` : ''; els.eventBody.innerHTML = `<div class="kv"><span>event_type</span><strong>${frame.event_type}</strong></div><div class="kv"><span>received</span><strong>${fmtTime(frame.received_at_wall)}</strong></div><div class="kv"><span>exchange</span><strong>${fmtTime(frame.exchange_ts)}</strong></div><div class="kv"><span>message</span><strong>${frame.message_index || ''}</strong></div><div class="kv"><span>asset</span><strong>${frame.outcome || frame.asset_id || '-'}</strong></div>${trade ? `<div class="changes"><strong>trade</strong>${trade}</div>` : ''}${changes ? `<div class="changes"><strong>changed levels</strong>${changes}</div>` : ''}`; els.delay.textContent = frame.delay_ms === null || frame.delay_ms === undefined ? 'delay -' : `delay ${Math.round(Number(frame.delay_ms))}ms`; }
function render() { if (!frames.length) return; const f = frames[idx]; buildTradeHistory(idx); for (const t of tokenIds) renderTeam(t, f); renderEvent(f); els.recvTime.textContent = fmtTime(f.received_at_wall); els.frameNo.textContent = `${idx + 1} / ${frames.length}`; els.slider.value = String(idx); }
function seekSeconds(deltaSeconds) { if (!frames.length) return; const current = new Date(frames[idx].received_at_wall).getTime(); if (!Number.isFinite(current)) return; const target = current + deltaSeconds * 1000; let lo = 0, hi = frames.length - 1, ans = 0; while (lo <= hi) { const mid = Math.floor((lo + hi) / 2); const t = new Date(frames[mid].received_at_wall).getTime(); if (t <= target) { ans = mid; lo = mid + 1; } else { hi = mid - 1; } } idx = Math.max(0, Math.min(frames.length - 1, ans)); render(); }
function stop() { playing = false; if (rafId) cancelAnimationFrame(rafId); rafId = null; lastTick = 0; frameCarry = 0; els.play.textContent = 'Play'; }
function tick(now) { if (!playing) return; if (!lastTick) lastTick = now; const elapsed = now - lastTick; lastTick = now; const frameMs = Math.max(20, 180 / Number(els.speed.value || 1)); frameCarry += elapsed / frameMs; const step = Math.min(8, Math.floor(frameCarry)); if (step > 0) { frameCarry -= step; idx = Math.min(frames.length - 1, idx + step); render(); if (idx >= frames.length - 1) { stop(); return; } } rafId = requestAnimationFrame(tick); }
function play() { if (playing) return stop(); playing = true; els.play.textContent = 'Pause'; rafId = requestAnimationFrame(tick); }
els.question.textContent = `${(payload.meta && payload.meta.slug) || ''} | ${(payload.meta && payload.meta.question) || ''}`;
for (const t of tokenIds) makeTeam(t);
els.slider.max = String(Math.max(0, frames.length - 1));
els.slider.addEventListener('input', () => { idx = Number(els.slider.value); render(); });
els.speed.addEventListener('change', () => { if (playing) { stop(); play(); } });
els.play.addEventListener('click', play);
els.back5.addEventListener('click', () => seekSeconds(-5));
els.step.addEventListener('click', () => { idx = Math.min(frames.length - 1, idx + 1); render(); });
render();
</script>
</body>
</html>
	__TITLE____DATA__)r   htmlescaper7   getr   )r:   datatitletemplates       r   r5   r5   P   sh    gDKKW[[06B;;JGY>Z[EnH^ K/77
DIIr   z--slugTzRecorded Polymarket market slug)requiredhelpz--data-rootzdata/lplF)	file_okay	path_typezRecording data root)defaultshow_defaulttyperG   z--output)dir_okayrI   zIOutput HTML path. Defaults to data/lpl/<slug>/event_orderbook_replay.html)rJ   rL   rG   z--depth   zLevels per sidez--market-kind)	moneylinegame1game2game3game4z8Export one target market from a unified event recording.z--frame-strider   zJKeep every Nth event frame. Use this only for very large browser payloads.c                    t        | |||||      }t        j                  d|d    d|d    d|d    d|d	           y )
N)r   r   r   r   r   r(   zevent viewer exported: frames=r'   z started_at=r-   z
 ended_at=r.   z output=r   )r;   r   info)r   r   r   r   r   r(   results          r   mainrW      st    L !!F KK	"# $\*+ ,:&' ('(		*r   __main__)r   objectreturnr7   )r   r7   r   r   r   Path | Noner   intr(   r\   r   
str | NonerZ   dict[str, object])r:   r^   rZ   r7   )r   r7   r   r   r   r[   r   r\   r   r]   r(   r\   rZ   None)__doc__
__future__r   r@   r   ossysr   r   pathlibr   clicklogurur   pathdirnameabspath__file__ROOTinsert#analysis.lpl_orderbook.event_replayr   r   r;   r5   commandoptionr\   ChoicerW   __name__ r   r   <module>rs      s)   #   	 
 '   	wwrwwrwwx'@ABsxxHHOOAt A  #*
* * 	*
 * * * *ZrJj h,MN	et	4	 	Ud	3	T iCFWX	G	H	C	 		U
  	
   
 Y O <2 zF r   