import fs from "node:fs";
|
import crypto from "node:crypto";
|
|
const authorityPath = process.argv[2];
|
const rawPath = process.argv[3];
|
const outputPath = process.argv[4];
|
if (!authorityPath || !rawPath || !outputPath) throw new Error("usage: node summary_derivation.mjs <authority.csv> <raw.csv> <output.csv>");
|
|
function parseCsv(text) {
|
const rows=[]; let row=[]; let cell=""; let quoted=false;
|
for (let i=0;i<text.length;i++) {
|
const ch=text[i];
|
if (quoted) {
|
if (ch==='"' && text[i+1]==='"') { cell+='"'; i++; }
|
else if (ch==='"') quoted=false;
|
else cell+=ch;
|
} else if (ch==='"') quoted=true;
|
else if (ch===',') { row.push(cell); cell=""; }
|
else if (ch==='\n') { row.push(cell.replace(/\r$/, "")); if (row.some((value)=>value!=="")) rows.push(row); row=[]; cell=""; }
|
else cell+=ch;
|
}
|
if (cell!=="" || row.length) { row.push(cell.replace(/\r$/, "")); rows.push(row); }
|
const headers=rows[0].map((value)=>value.replace(/^\uFEFF/, ""));
|
return rows.slice(1).map((values)=>Object.fromEntries(headers.map((header,index)=>[header,values[index]??""])));
|
}
|
function csvCell(value) { const text=value===null||value===undefined?"":String(value); return /[",\r\n]/.test(text)?'"'+text.replaceAll('"','""')+'"':text; }
|
function number(value) { const parsed=Number(value); return value!=="" && Number.isFinite(parsed) ? parsed : null; }
|
function round6(value) { return Math.round((value + Number.EPSILON) * 1e6) / 1e6; }
|
const authority=parseCsv(fs.readFileSync(authorityPath,"utf8"));
|
const rawBytes=fs.readFileSync(rawPath);
|
const rawHash=crypto.createHash("sha256").update(rawBytes).digest("hex");
|
const raw=parseCsv(rawBytes.toString("utf8"));
|
if (authority.length!==100 || new Set(authority.map((row)=>row.symbol)).size!==100) throw new Error("authority must contain 100 unique symbols");
|
const rawKeys=new Set();
|
for (const row of raw) { const key=row.symbol+"|"+row.trade_date; if (rawKeys.has(key)) throw new Error("duplicate raw key "+key); rawKeys.add(key); }
|
const bySymbol=new Map();
|
for (const row of raw) { if (!bySymbol.has(row.symbol)) bySymbol.set(row.symbol,[]); bySymbol.get(row.symbol).push(row); }
|
for (const rows of bySymbol.values()) rows.sort((a,b)=>a.trade_date.localeCompare(b.trade_date));
|
const headers=["case_id","action_id","company_market_id","symbol","company_id","canonical_name","universe_layer","priority_bucket","price_rows","close_computable_days","daily_return_computable_days","amount_ratio_computable_days","first_trade_date","last_trade_date","first_close","last_close","period_return_pct","strong_up_days","max_amount_ratio_20","metric_completeness_status","manifestation_type","gap_reason","source_latest_trade_date","raw_snapshot_sha256","raw_snapshot_bytes","summary_derivation_version","conclusion_strength","formal_pool_effect"];
|
const output=[];
|
for (const authorityRow of authority) {
|
const rows=bySymbol.get(authorityRow.symbol)??[];
|
const analysis=rows.filter((row)=>row.analysis_window_flag==="1");
|
const priceRows=analysis.length;
|
const closeDays=analysis.filter((row)=>{const v=number(row.close);return v!==null&&v>0;}).length;
|
const dailyReturnDays=analysis.filter((row)=>{const c=number(row.close),p=number(row.pre_close);return c!==null&&c>0&&p!==null&&p>0;}).length;
|
const ratios=[];
|
for (const current of analysis) {
|
const currentIndex=rows.findIndex((row)=>row.trade_date===current.trade_date);
|
const prior=rows.slice(Math.max(0,currentIndex-20),currentIndex).map((row)=>number(row.amount));
|
const currentAmount=number(current.amount);
|
if (prior.length===20 && prior.every((value)=>value!==null&&value>0) && currentAmount!==null && currentAmount>=0) ratios.push(currentAmount/(prior.reduce((sum,value)=>sum+value,0)/20));
|
}
|
const gap=[];
|
if (priceRows===0) gap.push("NO_PRICE_ROWS");
|
if (priceRows>0 && closeDays!==priceRows) gap.push("INSUFFICIENT_CLOSE");
|
if (priceRows>0 && dailyReturnDays!==priceRows) gap.push("INSUFFICIENT_DAILY_RETURN_BASE");
|
if (priceRows>0 && ratios.length!==priceRows) gap.push("INSUFFICIENT_AMOUNT_BASELINE");
|
const completeness=gap.length===0?"COMPLETE":gap.length===1?gap[0]:"INSUFFICIENT_MULTIPLE";
|
const first=analysis[0]; const last=analysis.at(-1);
|
const firstClose=first?number(first.close):null; const lastClose=last?number(last.close):null;
|
const periodReturn=completeness==="COMPLETE"?round6((lastClose/firstClose-1)*100):null;
|
const strongDays=completeness==="COMPLETE"?analysis.filter((row)=>round6((number(row.close)/number(row.pre_close)-1)*100)>=9.5).length:null;
|
const maxRatio=completeness==="COMPLETE"?round6(Math.max(...ratios)):null;
|
const manifestation=completeness!=="COMPLETE"?"INSUFFICIENT_DATA":(periodReturn>=20||strongDays>=1||maxRatio>=2)?"STRONG_MANIFESTATION":"WEAK_OR_NORMAL";
|
const latestDates=rows.map((row)=>row.source_latest_trade_date).filter(Boolean);
|
output.push(["ANA-ROBOT-INDUSTRY-001","NEXT-ROBOT-036",authorityRow.company_market_id,authorityRow.symbol,authorityRow.company_id,authorityRow.canonical_target_name,authorityRow.universe_layer,authorityRow.priority_bucket,priceRows,closeDays,dailyReturnDays,ratios.length,first?.trade_date??"",last?.trade_date??"",firstClose??"",lastClose??"",periodReturn??"",strongDays??"",maxRatio??"",completeness,manifestation,gap.length?gap.join("|"):"NONE",latestDates[0]??"",rawHash,rawBytes.length,"MARKET_SUMMARY_FROM_FROZEN_RAW_V2","MARKET_OBSERVATION_ONLY_NOT_INVESTMENT_CONCLUSION","NO_AUTOMATIC_FORMAL_POOL_CHANGE"]);
|
}
|
if (output.length!==100 || output.some((row)=>row[19]!=="COMPLETE" && row[20]!=="INSUFFICIENT_DATA")) throw new Error("summary completeness contract failure");
|
const csv="\uFEFF"+[headers,...output].map((row)=>row.map(csvCell).join(",")).join("\r\n")+"\r\n";
|
fs.writeFileSync(outputPath,csv,"utf8");
|