<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Check a forecasts file against the event contract</title>
<style>
:root{--bg:#fff;--fg:#1a1a1a;--mut:#555;--ok:#0a6b2d;--bad:#b00020;--line:#d0d0d0}
@media (prefers-color-scheme:dark){:root{--bg:#121212;--fg:#eee;--mut:#aaa;--ok:#5fd68a;--bad:#ff8a8a;--line:#444}}
body{margin:0 auto;max-width:46rem;padding:1rem 16px 3rem;background:var(--bg);color:var(--fg);font:16px/1.5 system-ui,sans-serif}
h1{font-size:1.3rem}textarea{width:100%;min-height:7rem;font:13px/1.4 ui-monospace,monospace;background:var(--bg);color:var(--fg);border:1px solid var(--line)}
button,input[type=file]{font:inherit;margin:.3rem 0}pre{white-space:pre-wrap;word-break:break-word;border:1px solid var(--line);padding:.6rem;font-size:13px}
.ok{color:var(--ok);font-weight:600}.bad{color:var(--bad);font-weight:600}.m{color:var(--mut);font-size:.9rem}
</style><!--sub:2e1db052--><link rel="manifest" href="/manifest.webmanifest"><meta name="theme-color" content="#120b20"><link rel="apple-touch-icon" href="/apple-touch-icon.png"><meta name="mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-title" content="Poly Trends"><script src="/sub.js?v=2e1db052" defer></script><!--/sub--><!--ga:90521b27--><script src="/consent.js?v=90521b27" defer></script><!--/ga--></head><body>
<h1>Check a forecasts file against the event contract</h1>
<p>Choose a CSV of forecasts (the columns of <a href="../council-forecasts.csv">council-forecasts.csv</a>) or paste it. The page reads it on your computer: nothing is sent anywhere. It runs the same check as
<a href="conformance.py">tools/conformance.py</a>, against <a href="../event-contract.schema.json">event-contract.schema.json</a>: the schema of every row, <code>event_id</code> equal to the hash of its contract,
the end later than the reference, the <code>ev2</code> id with its columns, <code>prob_up</code> between 0 and 100, the sign of the outcome, one id for one event.</p>
<p><input type="file" id="f" accept=".csv,text/csv,text/plain"></p>
<p><textarea id="t" placeholder="or paste the CSV here"></textarea></p>
<p><label><input type="checkbox" id="v2"> <code>--v2-required</code>: <code>event_id_v2</code> and its columns must be filled for every market that has a price source</label></p>
<p><button id="go">Check</button> <span id="s" class="m"></span></p>
<pre id="out" aria-live="polite"></pre>
<p class="m">Schema and rules of this page are those of version <span id="ver"></span> of conformance.py, written into the file at build time. The same check on your computer:
<code>python3 conformance.py your.csv</code>.</p>
<script>
var CFG={"contract2_keys": ["asset", "horizon", "reference_at", "end_at", "rule", "resolver_version", "reference_rule", "price_provider", "price_symbol"], "contract_keys": ["asset", "horizon", "reference_at", "end_at", "rule"], "csv_row": {"properties": {"actual_pct": {"pattern": "^(|-?[0-9]+(\\.[0-9]+)?([eE][-+]?[0-9]+)?)$", "type": "string"}, "asset": {"minLength": 1, "type": "string"}, "draw": {"pattern": "^(|True|False|true|false)$", "type": "string"}, "event_end_at": {"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$", "type": "string"}, "event_id": {"pattern": "^ev1-[0-9a-f]{16}$", "type": "string"}, "event_id_v2": {"description": "optional: the id of the full contract (contract_v2)", "pattern": "^(|ev2-[0-9a-f]{16})$", "type": "string"}, "event_reference_at": {"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$", "type": "string"}, "horizon": {"minLength": 1, "type": "string"}, "methodology_version": {"pattern": "^m[0-9]+$", "type": "string"}, "price_provider": {"type": "string"}, "price_symbol": {"type": "string"}, "prob_up": {"description": "stated chance of up, % (0-100), empty when none", "pattern": "^(|[0-9]+(\\.[0-9]+)?)$", "type": "string"}, "reference_rule": {"type": "string"}, "resolver_version": {"pattern": "^(|r[0-9]+)$", "type": "string"}}, "required": ["event_id", "event_reference_at", "event_end_at", "asset", "horizon", "methodology_version"], "type": "object"}, "default_resolver": "r1", "price_provider": "yahoo_finance_chart_v8_1m", "price_sources": {"BRENT": "BZ=F", "BTC": "BTC-USD", "COPPER": "HG=F", "DJI": "^DJI", "DXY": "DX-Y.NYB", "GOLD": "GC=F", "NDX": "^NDX", "SOX": "^SOX", "SPX": "^GSPC", "US10Y": "^TNX", "VIX": "^VIX", "WTI": "CL=F"}, "reference_rules": ["first_1m_bar_at_or_after_reference_at__last_1m_bar_at_or_before_end_at__v1"], "rule": "up_if_close_above_reference; draw_if_abs_move_below_band", "version": "3"};
function sha256hex(str){
  var b=new TextEncoder().encode(str),l=b.length,K=[],H=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],i,j,p=2,n=0;
  function rr(x,c){return (x>>>c)|(x<<(32-c))}
  while(n<64){var pr=true;for(i=2;i*i<=p;i++){if(p%i===0){pr=false;break}}if(pr){K[n++]=Math.floor((Math.pow(p,1/3)%1)*4294967296)|0}p++}
  var nb=((l+9+63)>>6)<<6,m=new Uint8Array(nb);m.set(b);m[l]=0x80;
  var hi=Math.floor(l/536870912),lo=(l<<3)>>>0;
  m[nb-8]=hi>>>24;m[nb-7]=hi>>>16;m[nb-6]=hi>>>8;m[nb-5]=hi;m[nb-4]=lo>>>24;m[nb-3]=lo>>>16;m[nb-2]=lo>>>8;m[nb-1]=lo;
  var w=new Array(64);
  for(var o=0;o<nb;o+=64){
    for(i=0;i<16;i++)w[i]=(m[o+4*i]<<24)|(m[o+4*i+1]<<16)|(m[o+4*i+2]<<8)|m[o+4*i+3];
    for(i=16;i<64;i++){var s0=rr(w[i-15],7)^rr(w[i-15],18)^(w[i-15]>>>3),s1=rr(w[i-2],17)^rr(w[i-2],19)^(w[i-2]>>>10);w[i]=(w[i-16]+s0+w[i-7]+s1)|0}
    var a=H[0],bb=H[1],c=H[2],d=H[3],e=H[4],f=H[5],g=H[6],h=H[7];
    for(i=0;i<64;i++){
      var S1=rr(e,6)^rr(e,11)^rr(e,25),ch=(e&f)^(~e&g),t1=(h+S1+ch+K[i]+w[i])|0,S0=rr(a,2)^rr(a,13)^rr(a,22),mj=(a&bb)^(a&c)^(bb&c),t2=(S0+mj)|0;
      h=g;g=f;f=e;e=(d+t1)|0;d=c;c=bb;bb=a;a=(t1+t2)|0}
    H[0]=(H[0]+a)|0;H[1]=(H[1]+bb)|0;H[2]=(H[2]+c)|0;H[3]=(H[3]+d)|0;H[4]=(H[4]+e)|0;H[5]=(H[5]+f)|0;H[6]=(H[6]+g)|0;H[7]=(H[7]+h)|0}
  var r='';for(i=0;i<8;i++){r+=('00000000'+(H[i]>>>0).toString(16)).slice(-8)}return r}
function canon(c,keys){var o=[];keys.slice().sort().forEach(function(k){o.push(JSON.stringify(k)+':'+JSON.stringify(c[k]))});return '{'+o.join(',')+'}'}
function eventId(a,h,r,e){return 'ev1-'+sha256hex(canon({asset:String(a),horizon:String(h),reference_at:String(r),end_at:String(e),rule:CFG.rule},CFG.contract_keys)).slice(0,16)}
function priceSource(a){var t=CFG.price_sources[String(a)];return t?{provider:CFG.price_provider,symbol:t}:null}
function eventId2(a,h,r,e,rv,rr_,pp,ps){
  var src=priceSource(a)||{};
  var c={asset:String(a),horizon:String(h),reference_at:String(r),end_at:String(e),rule:CFG.rule,resolver_version:String(rv||CFG.default_resolver),reference_rule:String(rr_),
         price_provider:String(pp||src.provider||''),price_symbol:String(ps||src.symbol||'')};
  return 'ev2-'+sha256hex(canon(c,CFG.contract2_keys)).slice(0,16)}
function parseCSV(text){
  var rows=[],row=[],f='',q=false,i=0,n=text.length,ch;
  if(text.charCodeAt(0)===0xFEFF)i=1;
  for(;i<n;i++){ch=text[i];
    if(q){if(ch==='"'){if(text[i+1]==='"'){f+='"';i++}else q=false}else f+=ch}
    else if(ch==='"')q=true;
    else if(ch===','){row.push(f);f=''}
    else if(ch==='\n'||ch==='\r'){if(ch==='\r'&&text[i+1]==='\n')i++;row.push(f);rows.push(row);row=[];f=''}
    else f+=ch}
  if(f!==''||row.length){row.push(f);rows.push(row)}
  return rows.filter(function(r){return !(r.length===1&&r[0]==='')})}
function validateRow(v,sch){
  var errs=[],props=sch.properties||{};
  (sch.required||[]).forEach(function(k){if(!(k in v))errs.push('$: missing field '+k)});
  Object.keys(v).forEach(function(k){var p=props[k];if(!p)return;var s=v[k];
    if(p.minLength&&s.length<p.minLength)errs.push('$.'+k+': empty');
    if(p.pattern&&!new RegExp(p.pattern).test(s))errs.push('$.'+k+': '+JSON.stringify(s)+' does not match '+p.pattern)});
  return errs}
function checkRow(row,line,v2){
  var tag=line?'line '+line:'row',errs=validateRow(row,CFG.csv_row).map(function(e){return tag+': '+e});
  if(errs.length)return errs;
  var want=eventId(row.asset,row.horizon,row.event_reference_at,row.event_end_at);
  if(row.event_id!==want)errs.push(tag+': event_id '+row.event_id+' is not the hash of its contract ('+want+')');
  if(row.event_end_at<=row.event_reference_at)errs.push(tag+': event_end_at is not later than event_reference_at');
  if(v2&&!row.event_id_v2&&priceSource(row.asset))errs.push(tag+': event_id_v2 is required (--v2-required) and is empty for '+row.asset);
  if(row.event_id_v2){
    var miss=['reference_rule','price_provider','price_symbol'].filter(function(k){return !row[k]});
    if(miss.length)errs.push(tag+': event_id_v2 given but ['+miss.join(', ')+'] empty');
    else if(CFG.reference_rules.indexOf(row.reference_rule)<0)errs.push(tag+': reference_rule '+JSON.stringify(row.reference_rule)+' is not one of '+JSON.stringify(CFG.reference_rules));
    else{
      var w2=eventId2(row.asset,row.horizon,row.event_reference_at,row.event_end_at,row.resolver_version||null,row.reference_rule,row.price_provider,row.price_symbol);
      if(row.event_id_v2!==w2)errs.push(tag+': event_id_v2 '+row.event_id_v2+' is not the hash of its full contract ('+w2+')');
      var ours=priceSource(row.asset);
      if(ours&&row.price_provider===CFG.price_provider&&row.price_symbol!==ours.symbol)errs.push(tag+': price_symbol '+row.price_symbol+' for '+row.asset+' (the list has '+ours.symbol+')')}}
  var p=row.prob_up||'';
  if(p!==''&&!(0<=parseFloat(p)&&parseFloat(p)<=100))errs.push(tag+': prob_up '+p+' outside 0-100');
  var a=row.actual_pct||'',d=(row.draw||'').toLowerCase();
  if(a!==''&&d==='')errs.push(tag+': actual_pct without a draw flag');
  if(d==='true'&&a==='')errs.push(tag+': draw without actual_pct');
  if(d==='false'&&a!==''&&parseFloat(a)===0)errs.push(tag+': a zero move is a draw, not up or down');
  return errs}
function checkCSV(text,v2,maxErrors){
  maxErrors=maxErrors||50;var data=parseCSV(text),errs=[],n=0,seen={};
  var need=['event_id','event_reference_at','event_end_at','asset','horizon','methodology_version'];
  if(v2)need=need.concat(['event_id_v2','reference_rule','price_provider','price_symbol']);
  var hdr=data.length?data[0]:[],miss=need.filter(function(k){return hdr.indexOf(k)<0}).sort();
  if(!hdr.length||miss.length)return {rows:0,errors:['header: missing columns ['+miss.join(', ')+']'],ok:false};
  for(var i=1;i<data.length;i++){
    var line=i+1,row={};n++;
    hdr.forEach(function(k,j){row[k]=data[i][j]||''});
    errs=errs.concat(checkRow(row,line,v2));
    var key=row.event_id,sig=[row.asset,row.horizon,row.event_reference_at,row.event_end_at].join('\u0001');
    if(key in seen){if(seen[key]!==sig)errs.push('line '+line+': event_id '+key+' is used for two different events')}else seen[key]=sig;
    if(errs.length>=maxErrors){errs.push('... stopped at '+maxErrors+' errors');break}}
  return {rows:n,errors:errs,ok:!errs.length}}
if(typeof document!=='undefined'){
  document.getElementById('ver').textContent=CFG.version;
  var T=document.getElementById('t');
  document.getElementById('f').addEventListener('change',function(e){var fl=e.target.files[0];if(!fl)return;var r=new FileReader();r.onload=function(){T.value=r.result;document.getElementById('s').textContent=fl.name+' read'};r.readAsText(fl)});
  document.getElementById('go').addEventListener('click',function(){
    var o=document.getElementById('out'),res=checkCSV(T.value,document.getElementById('v2').checked);
    o.textContent=(res.ok?'OK: ':'')+res.rows+' rows'+(res.ok?'\n':', '+res.errors.length+' problems\n')+res.errors.join('\n');
    o.className=res.ok?'ok':'bad'})}
if(typeof module!=='undefined')module.exports={checkCSV:checkCSV,checkRow:checkRow,sha256hex:sha256hex,eventId:eventId,eventId2:eventId2};
</script></body></html>
