// Offline causal reconstruction only. No trading, MT5 calls or execution model.
const fs=require('fs'),path=require('path'),crypto=require('crypto');
const root=path.join(__dirname,'reproduced'),dataRoot=path.join(__dirname,'sources');
fs.mkdirSync(root,{recursive:true});
const sha=b=>crypto.createHash('sha256').update(b).digest('hex');
const provenance=JSON.parse(fs.readFileSync(path.join(dataRoot,'provenance.json'),'utf8'));
function load(symbol,tf){const file=symbol+'-'+tf+'.csv',b=fs.readFileSync(path.join(dataRoot,file));if(sha(b)!==provenance.datasets.find(d=>d.file===file).sha256)throw Error('Source SHA mismatch');const lines=b.toString('utf8').trim().split(/\r?\n/),keys=lines.shift().split(',');return lines.map(l=>Object.fromEntries(l.split(',').map((v,i)=>[keys[i],keys[i]==='time_mt5'?v:Number(v)])));}
const iso=t=>new Date(t*1000).toISOString().slice(0,16).replace('T',' '); // Encoding only; do not label MT5 wall-clock as UTC.
const aggregate=rows=>({open:rows[0].open,high:Math.max(...rows.map(r=>r.high)),low:Math.min(...rows.map(r=>r.low)),close:rows.at(-1).close});
const equal=(a,b)=>['open','high','low','close'].every(k=>Math.abs(a[k]-b[k])<1e-10);
const h1=load('EURUSD','H1'),h4=load('EURUSD','H4'),byTime=new Map(h1.map(r=>[r.time_raw_epoch,r]));
let mtf;
for(let j=1;j
byTime.get(start+k*3600));
const previousRows=[0,1,2,3].map(k=>byTime.get(prior.time_raw_epoch+k*3600));
if([...rows,...previousRows].some(r=>!r))continue;
const partial=aggregate(rows.slice(0,2)),final=aggregate(rows);
if(partial.close>prior.close&&final.close<=prior.close){
if(!equal(final,b)||!equal(aggregate(previousRows),prior))throw Error('H1/H4 mismatch in selected case');
mtf={symbol:'EURUSD',timeBasis:provenance.time_basis,start:iso(start),end:iso(start+14400),midpoint:iso(start+7200),priorStart:prior.time_mt5,priorEnd:iso(start),prior,rows,previousRows,partial,final,threshold:prior.close,sourceH4:b,h1H4ExactMatch:true,asOf:[{decision:iso(start+7200),lastCompleteH4:prior.time_mt5,filter:prior.close>prior.close,usedClose:prior.close,provisionalClose:partial.close,provisionalPass:true,finalCurrentH4Allowed:false},{decision:iso(start+14400),lastCompleteH4:b.time_mt5,filter:final.close>prior.close,usedClose:final.close,provisionalClose:null,provisionalPass:null,finalCurrentH4Allowed:true}],availability:'Offline earliest eligibility at interval end; actual received_at and live revisions are not in this export. All labels use original MT5 time.'};break;
}
}
if(!mtf)throw Error('No predetermined MTF geometry case found');
function scan(rows,symbol){
const out=[],issued=new Set();let latest=null;
for(let t=0;t=2&&[i-2,i-1,i+1,i+2].every(k=>rows[i].high>rows[k].high)&&rows.slice(i-2,t+1).every((r,k,a)=>!k||r.time_raw_epoch-a[k-1].time_raw_epoch===3600))latest={i,price:rows[i].high,centre:rows[i].time_mt5,confirmedAt:iso(rows[i+2].time_raw_epoch+3600),confirmedIndex:i+2};
const r=rows[t],prev=rows[t-1];let state='NO_LEVEL';
const continuous=!!prev&&r.time_raw_epoch-prev.time_raw_epoch===3600;
if(!continuous){latest=null;state='GAP_RESET';}
else if(latest){
if(t-latest.i>24)state='EXPIRED';
else if(issued.has(latest.centre))state='ALREADY_REPORTED';
else if(r.close>latest.price&&prev.close<=latest.price){state='CANDIDATE';issued.add(latest.centre);}
else if(r.high>latest.price&&r.close<=latest.price)state='REJECT_CLOSE';
else state='NO_CROSS';
}
out.push({symbol,index:t,time_mt5:r.time_mt5,eligible_not_before:iso(r.time_raw_epoch+3600),open:r.open,high:r.high,low:r.low,close:r.close,previous_close:prev?.close??null,pivot_centre:latest?.centre??null,pivot_index:latest?.i??null,pivot_high:latest?.price??null,pivot_confirmed_at:latest?.confirmedAt??null,pivot_confirmed_index:latest?.confirmedIndex??null,state,key:'pivot-break-v1|ICMarketsSC-Demo|'+symbol+'|H1|'+r.time_mt5});
}
return out;
}
const universe=['EURUSD','GBPUSD','USDJPY'];
const streams=Object.fromEntries(universe.map(s=>[s,load(s,'H1')]));
const logs=Object.fromEntries(universe.map(s=>[s,scan(streams[s],s)]));
const eligible=logs.EURUSD.filter(r=>r.time_mt5>='2026-10-01 00:00'&&r.state==='CANDIDATE');
let selected;
for(const candidate of eligible){const same=logs.EURUSD.filter(r=>r.index>=candidate.index-12&&r.index High[i-2], High[i-1], High[i+1], High[i+2]; equal highs rejected',pivotKnown:'after i+2 closes; the level must be confirmed before the evaluated bar starts',event:'C[t] > R AND C[t-1] <= R',tolerance:0,expiry:'24 subsequent observed H1 bars from centre; expired when t-i > 24',deduplication:'one candidate per pivot centre; repeat key returns the recorded outcome',gap:'Any non-hourly step resets context; no inference about why data are absent; rebuild a new contiguous 2+2 pivot',availability:'end is only an earliest-eligibility bound, not measured received_at; export cannot prove the live record or notification time',selection:'First EURUSD candidate on/after 2026-10-01 with an earlier REJECT_CLOSE under the same pivot within the prior 12 bars; selected for teaching availability, not returns',...selected,rows:streams.EURUSD.slice(startIndex,endIndex+1),trace:logs.EURUSD.slice(startIndex,endIndex+1),snapshot:universe.map(s=>logs[s].find(r=>r.time_mt5===selected.candidate.time_mt5)||{symbol:s,state:'NO_DATA'})};
const assertions=[];function check(name,pass){assertions.push({name,pass});if(!pass)throw Error(name);}
check('MTF final H4 matches source H4 exactly',equal(mtf.final,mtf.sourceH4));
check('MTF prior H4 matches source H1 aggregation',equal(aggregate(mtf.previousRows),mtf.prior));
check('MTF partial passes but completed block rejects',mtf.partial.close>mtf.threshold&&mtf.final.close<=mtf.threshold);
check('No current final H4 at midpoint',mtf.asOf[0].finalCurrentH4Allowed===false);
const allLog=Object.values(logs).flat();
check('Pivot available before evaluated bar',allLog.filter(r=>r.pivot_index!==null).every(r=>r.pivot_confirmed_indexr.state==='CANDIDATE').every(r=>r.close>r.pivot_high&&r.previous_close<=r.pivot_high));
check('Displayed rejected touch fails close operator',screen.rejected.high>screen.rejected.pivot_high&&screen.rejected.close<=screen.rejected.pivot_high);
check('Unique event keys',new Set(allLog.map(r=>r.key)).size===allLog.length);
for(const symbol of universe){const full=streams[symbol];for(let n=Math.max(10,screen.candidate.index-10);n<=Math.min(full.length,screen.candidate.index+3);n++){check('Prefix invariance '+symbol+' '+n,JSON.stringify(scan(full.slice(0,n),symbol))===JSON.stringify(logs[symbol].slice(0,n)));}}
const put=(p,b)=>{fs.mkdirSync(path.dirname(p),{recursive:true});fs.writeFileSync(p,b);};
const csv=rows=>{const keys=Object.keys(rows[0]);return keys.join(',')+'\n'+rows.map(r=>keys.map(k=>r[k]??'').join(',')).join('\n')+'\n';};
put(path.join(root,'cases.json'),JSON.stringify({mtf,screen},null,2));
put(path.join(root,'qa-computations.json'),JSON.stringify({status:'PASS',assertions,sourceIntegrity:'32 source CSV verified separately and every file used reverified before load',noOrders:true,liveReceivedAtKnown:false,profitabilityBacktest:false},null,2));
put(path.join(root,'data','mtf-h1-source.csv'),csv([...mtf.previousRows,...mtf.rows]));
put(path.join(root,'data','mtf-h4-source.csv'),csv([mtf.prior,mtf.sourceH4]));
put(path.join(root,'data','screening-chart-source.csv'),csv(screen.rows));
put(path.join(root,'data','screening-trace.csv'),csv(screen.trace));
put(path.join(root,'data','screening-all-outcomes.csv'),csv(allLog.filter(r=>r.time_mt5>='2026-10-01 00:00'&&r.time_mt5<=screen.candidate.time_mt5)));
console.log(JSON.stringify({mtf:{start:mtf.start,partial:mtf.partial,final:mtf.final,threshold:mtf.threshold},screen:{candidate:screen.candidate,rejected:screen.rejected,snapshot:screen.snapshot},tests:assertions.length},null,2));