Cai
2 days ago 22384865fbdb92c4ce603c137b1cac52ab6450ba
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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");