// gas_report.gs - Report & template generation
// getDailyBrief, getSummaryJson, getTradeTemplate
// Changes only when report format changes. Rarely touched during engine work.
// GAS global scope: functions in gas_lib.gs / gas_data_feed.gs callable directly


// ── E1: 일일 의사결정 브리핑 ─────────────────────────────────────────────────
// 시장 상태·포트폴리오 건강·액션 목록·주의 종목·7일 이벤트를 한 JSON으로 통합.
// doGet(?view=brief) 또는 cacheAllViews()에서 매일 1회 생성.
function getDailyBrief(sellPriorityViewInput) {
  const macro    = getMacroJson();
  const settings = readSettingsTab_();
  const port     = getPortfolioJson();
  const events   = getEventRiskJson();
  const today    = Utilities.formatDate(new Date(), "Asia/Seoul", "yyyy-MM-dd");
  const holdings = port.holdings ?? [];

  // ── 액션 분류: Final_Action canonical 기준 (A-1/B-1 — Allowed_Action 기반 제거) ──
  // Final_Action이 canonical output field. Allowed_Action은 중간 계산값.
  const BUY_FINALS_  = new Set(["BUY_STAGE1_READY","BUY_BREAKOUT_PILOT_ONLY","BUY_PULLBACK_WAIT"]);
  const SELL_FINALS_ = new Set(["SELL_READY"]);
  const EXIT_FINALS_ = new Set(["EXIT_SIGNAL","EXIT_REVIEW"]);

  const sellList   = holdings.filter(h => SELL_FINALS_.has(h.Final_Action));
  const exitList   = holdings.filter(h => EXIT_FINALS_.has(h.Final_Action));
  const buyList    = holdings.filter(h => BUY_FINALS_.has(h.Final_Action));
  const watchList  = holdings.filter(h => h.Final_Action === "WATCH_TIMING_SETUP");
  const holdList   = holdings.filter(h =>
    !SELL_FINALS_.has(h.Final_Action) && !EXIT_FINALS_.has(h.Final_Action) &&
    !BUY_FINALS_.has(h.Final_Action)  && h.Final_Action !== "WATCH_TIMING_SETUP"
  );

  // 주의 종목
  const stage2Pass  = holdings.filter(h => h.Stage2_Gate  === "PASS");
  const timeStopNear= holdings.filter(h => Number.isFinite(+h.Days_To_Time_Stop)
                                        && +h.Days_To_Time_Stop >= 0
                                        && +h.Days_To_Time_Stop <= 7);
  const overweight  = holdings.filter(h => h.Band_Status  === "OVERWEIGHT");
  const tp1Near     = holdings.filter(h => Number.isFinite(+h.Profit_Pct) && +h.Profit_Pct >= 10);

  // 포트폴리오 건강 판단
  const heatVal     = parseFloat(macro.total_heat_pct);
  const fcVal       = parseFloat(macro.fc_budget_pct);
  const heatOk      = Number.isFinite(heatVal) && heatVal < 10;
  const heatCautionB= Number.isFinite(heatVal) && heatVal >= 7 && heatVal < 10;
  const heatBlockB  = Number.isFinite(heatVal) && heatVal >= 10;
  const fcOk        = Number.isFinite(fcVal) && fcVal < 100;
  const regimeStr   = String(macro.market_regime ?? "");
  const isRiskOffB  = regimeStr === "RISK_OFF" || regimeStr === "RISK_OFF_CANDIDATE";
  const nrf     = macro.net_return_feedback;
  const orbitAdj= parseInt(macro.orbit_slot_adj) || 0;

  // account_snapshot freshness 체크
  const acctFresh = checkAccountSnapshotFreshness_();

  // 텍스트 브리핑 (ChatGPT 직접 복붙용)
  const L = [];
  const hardBlockWarn = String(settings["cash_floor_hard_block_warning"] ?? "").trim();
  const accountConfirmWarn = String(settings["account_snapshot_confirmation_warning"] ?? "").trim();
  const cashLedgerWarn = String(settings["cash_ledger_warning"] ?? "").trim();
  if (hardBlockWarn) L.push(`[긴급 경고] ${hardBlockWarn}`);
  if (accountConfirmWarn) L.push(`[운영 경고] ${accountConfirmWarn}`);
  if (cashLedgerWarn) L.push(`[운영 경고] ${cashLedgerWarn}`);
  L.push(`[시장] ${macro.market_regime} / MRS ${macro.mrs_score}/10 / VIX ${macro.vix} / KOSPI ${macro.kospi} / USD/KRW ${macro.usd_krw}`);
  const heatTag = heatBlockB ? "⚠HF005:BLOCK" : heatCautionB ? "⚠CAUTION:수량50%감액" : "OK";
  L.push(`[포트폴리오] HEAT ${macro.total_heat_pct}%(${heatTag}) / FC ${macro.fc_budget_pct}%(${fcOk?"OK":"⚠EXHAUSTED"}) / ${nrf} / BUCKET ${macro.bucket_status}`);
  if (isRiskOffB) L.push(`[⚠ 레짐 차단] ${regimeStr} — 신규 매수 전면 차단, 보유 종목 50% 단계 축소 검토`);
  const bayesSourceTag = macro.bayesian_data_source === "actual" ? "실제거래기반" : "기본값(거래이력없음)";
  L.push(`[Bayesian] ${macro.bayesian_label} (${macro.bayesian_multiplier}×) — ${bayesSourceTag}`);
  if (acctFresh.fresh === false) L.push(`[⚠ account_snapshot STALE] ${acctFresh.reason} — 손절가·수량 재확인 필요`);
  else if (acctFresh.fresh === null) L.push(`[⚠ account_snapshot] ${acctFresh.reason}`);

  // 데이터 신선도 경고 — PRICE_STALE / PRICE_QUOTE_ONLY / FLOW_STALE
  const priceStaleList_  = holdings.filter(h => h.Price_Status === "PRICE_STALE");
  const quoteOnlyList_   = holdings.filter(h => h.Price_Status === "PRICE_QUOTE_ONLY");
  const flowStaleList_   = holdings.filter(h => String(h.Missing_Fields ?? "").includes("FLOW_STALE"));
  if (priceStaleList_.length)
    L.push(`[⚠ 가격 스테일] ${priceStaleList_.map(h => h.Name).join(", ")} — OHLC 날짜 오래됨, runDataFeed 재실행 권장`);
  if (quoteOnlyList_.length)
    L.push(`[⚠ 호가전용] ${quoteOnlyList_.map(h => h.Name).join(", ")} — OHLC 수집 실패, MA/ATR 결측 → OBSERVE_ONLY 처리`);
  if (flowStaleList_.length)
    L.push(`[⚠ 수급 스테일] ${flowStaleList_.map(h => h.Name).join(", ")} — 외국인/기관 수급 날짜 오래됨`);

  if (orbitAdj !== 0)
    L.push(`[Orbit] ${macro.orbit_state} → 공격슬롯 ${orbitAdj>0?"+":""}${orbitAdj}개 / 현금조정 ${macro.orbit_cash_adj}%p`);
  // ── C-1: Final_Action 기준 단일 우선순위 목록 ─────────────────────────────
  // 우선순위 순서: SELL_READY > EXIT_* > BUY > WATCH > HOLD
  // 같은 그룹 내에서는 Final_Rank(Priority_Score) 오름차순
  const byRank = (arr) => [...arr].sort((a, b) => (+a.Final_Rank || 999) - (+b.Final_Rank || 999));

  L.push("─".repeat(44));
  L.push(`[오늘 액션] — ${today} (Final_Action 기준, 우선순위 정렬)`);

  if (sellList.length) {
    L.push("  ▶ SELL_READY (즉시 HTS 주문 가능)");
    byRank(sellList).forEach((h, i) => {
      const r = h.Action_Reason || `${h.Sell_Action} ${h.Sell_Qty}주 @${h.Sell_Limit_Price}`;
      const p = h.Action_Params ? `\n       ${h.Action_Params}` : "";
      L.push(`    ${i+1}. ${h.Name} → ${r}${p}`);
    });
  }
  if (exitList.length) {
    L.push("  ▶ EXIT_SIGNAL / REVIEW (캡처 → ChatGPT 수량 계산 후 매도)");
    byRank(exitList).forEach((h, i) => {
      const r = h.Action_Reason || `${h.Final_Action}(RW${h.RW_Partial})`;
      const p = h.Action_Params ? ` | ${h.Action_Params}` : "";
      L.push(`    ${sellList.length+i+1}. ${h.Name}[${h.Final_Action}] → ${r}${p}`);
    });
  }
  if (buyList.length) {
    L.push("  ▶ BUY (진입 조건 충족)");
    byRank(buyList).forEach((h, i) => {
      const constr  = h.Pos_Size_Constraint || "미계산*";
      const rank_   = sellList.length + exitList.length + i + 1;
      L.push(`    ${rank_}. ${h.Name}[${h.Final_Action}] → ${h.Action_Reason || ""}`);
      const params_ = h.Action_Params || `목표 ${h.Pos_Size_Qty}주[${constr}]`;
      L.push(`       ${params_}`);
    });
  }
  if (watchList.length) {
    L.push("  ▶ WATCH (타이밍 대기)");
    byRank(watchList).forEach((h, i) => {
      const rank_ = sellList.length + exitList.length + buyList.length + i + 1;
      L.push(`    ${rank_}. ${h.Name} → ${h.Action_Reason || `SS001:${h.SS001_Grade} 타이밍미충족`}`);
    });
  }
  if (holdList.length) {
    L.push("  ▶ HOLD / BLOCK");
    byRank(holdList).forEach((h, i) => {
      const rank_ = sellList.length + exitList.length + buyList.length + watchList.length + i + 1;
      L.push(`    ${rank_}. ${h.Name}[${h.Allowed_Action}] → ${h.Action_Reason || h.Allowed_Action}`);
    });
  }
  if (!sellList.length && !exitList.length && !buyList.length && !watchList.length)
    L.push("  HOLD — 오늘 액션 없음");

  // 단일 진실원천: sell_priority는 반드시 runSellPriority() 결과만 사용
  const sellPriorityView_ = sellPriorityViewInput || runSellPriority();
  const _cashRaiseCands_ = Array.isArray(sellPriorityView_.sell_priority_table)
    ? sellPriorityView_.sell_priority_table
    : [];

  const _cashBelowTgt_ = isRiskOffB || (() => {
    const cp = parseFloat(macro.immediate_cash_pct ?? macro.cash_pct ?? "");
    const tp = parseFloat(macro.target_cash_pct ?? settings["weekly_target_cash_pct"] ?? "10");
    return Number.isFinite(cp) && Number.isFinite(tp) && cp < tp;
  })();

  if (_cashBelowTgt_ && _cashRaiseCands_.length) {
    L.push("─".repeat(44));
    const gapReason = isRiskOffB
      ? `REGIME_TRIM_50 발동(${regimeStr})`
      : `현금 부족 → sell_priority_engine`;
    L.push(`[현금확보 매도우선순위] — ${gapReason}`);
    L.push("  spec: ①하드스탑>②매도신호>③중복ETF>④손실위성>⑥익절>⑨코어주도주(마지막)");
    L.push("  ⚠ 매도수량은 HTS 캡처 제공 후 결정 — 수량 미제공 시 수량 산출 금지(P1규칙)");
    _cashRaiseCands_.slice(0, 8).forEach((c, i) => {
      const pStr = (c.profit_pct !== "" && c.profit_pct !== null)
        ? ` (${Number(c.profit_pct) >= 0 ? "+" : ""}${Number(c.profit_pct).toFixed(1)}%)`
        : "";
      const etfTag = c.is_etf  ? "[ETF]"    : "";
      const clTag  = c.is_core_leader ? "[주도주⛔매도금지]" : "";
      L.push(`  ${i+1}. ${c.tier_label} ${c.name}${etfTag}${clTag} W:${c.weight_pct}%${pStr} RW:${c.rw_partial} Score:${c.sell_priority_score}`);
      if (c.trim_style || c.rebound_holdback_score)
        L.push(`     └ trim=${c.trim_style || "N/A"} rebound_holdback=${c.rebound_holdback_score ?? 0}${c.rebound_holdback_reason ? ` | ${c.rebound_holdback_reason}` : ""}`);
      if (c.action_params) L.push(`     └ ${c.action_params}`);
      if (c.hold_reason)   L.push(`     └ ⚠ ${c.hold_reason}`);
    });
  }

  // 주의 종목 섹션
  if (stage2Pass.length || timeStopNear.length || overweight.length || tp1Near.length) {
    L.push("[주의]");
    stage2Pass.forEach(h   => L.push(`  ${h.Name} Stage2_Gate=PASS → 2단계 진입 검토 (진입가 ${h.Limit_Price_Est ?? "N/A"})`));
    timeStopNear.forEach(h => L.push(`  ${h.Name} Time_Stop ${h.Days_To_Time_Stop}일 남음 (${h.Time_Stop_Date})`));
    overweight.forEach(h   => L.push(`  ${h.Name} OVERWEIGHT ${h.Weight_Pct}% (상한 7%)`));
    tp1Near.forEach(h      => L.push(`  ${h.Name} +${h.Profit_Pct}% → TP1(${h.TP1_Price}원) 근접`));
  }
  if (events.upcoming_7d?.length) {
    L.push("[7일 이벤트]");
    events.upcoming_7d.forEach(ev => L.push(`  ${ev.Date}(D+${ev.DaysLeft}) ${ev.Event} [${ev.Impact}]`));
  }

  // brief_ — holdings row → JSON 요약 (API 소비자용)
  const brief_ = (h) => ({
    ticker: h.Ticker, name: h.Name,
    final_action: h.Final_Action,   // canonical output field
    action_reason: h.Action_Reason, // 왜 이 액션인가
    action_params: h.Action_Params, // 실행 파라미터 압축 (C-3)
    final_rank: h.Final_Rank,
    allowed_action: h.Allowed_Action,
    ss001_grade: h.SS001_Grade, ss001_norm_score: h.SS001_Norm_Score,
    rw_partial: h.RW_Partial,
    weight_pct: h.Weight_Pct,  profit_pct: h.Profit_Pct,
    stage2_gate: h.Stage2_Gate, band_status: h.Band_Status,
    limit_price_est: h.Limit_Price_Est,
    stop_price_est: h.Stop_Price_Est, stop_price_source: h.Stop_Price_Source,
    pos_size_qty: h.Pos_Size_Qty, pos_size_constraint: h.Pos_Size_Constraint,
    tp1_price: h.TP1_Price, tp1_qty: h.TP1_Qty,
    tp2_price: h.TP2_Price, tp2_qty: h.TP2_Qty,
    entry_mode: h.Entry_Mode, entry_mode_gate: h.Entry_Mode_Gate,
    entry_mode_reason: h.Entry_Mode_Reason,
    timing_score_entry: h.Timing_Score_Entry,
    timing_score_exit: h.Timing_Score_Exit,
    timing_action: h.Timing_Action,
    timing_block_reason: h.Timing_Block_Reason,
    sell_action: h.Sell_Action,
    sell_ratio_pct: h.Sell_Ratio_Pct,
    sell_limit_price: h.Sell_Limit_Price,
    sell_reason: h.Sell_Reason,
    sell_validation: h.Sell_Validation,
    cash_preserve_style: h.Cash_Preserve_Style || "",
    cash_preserve_ratio: h.Cash_Preserve_Ratio || "",
    cash_preserve_reason: h.Cash_Preserve_Reason || "",
    rsi14: h.RSI14, disparity: h.Disparity, ma20_slope: h.MA20_Slope,
    exit_signal_detail: h.Exit_Signal_Detail,
  });

  return {
    date: today,
    brief_text: L.join("\n"),
    market: {
      regime: macro.market_regime, mrs_score: macro.mrs_score,
      vix: macro.vix, kospi: macro.kospi, usd_krw: macro.usd_krw,
      sp500_ret5d: macro.sp500_ret5d,
    },
    portfolio_health: {
      heat_pct: macro.total_heat_pct, heat_ok: heatOk,
      heat_tag: heatTag,
      heat_block: heatBlockB, heat_caution: heatCautionB,
      fc_budget_pct: macro.fc_budget_pct, fc_ok: fcOk,
      net_return_feedback: nrf,
      bucket_status: macro.bucket_status,
      regime_buy_blocked: isRiskOffB,
      bayesian_label: macro.bayesian_label,
      bayesian_multiplier: macro.bayesian_multiplier,
    },
    orbit: {
      gap_pct: macro.orbit_gap_pct, state: macro.orbit_state,
      slot_adjustment: orbitAdj,    cash_adjustment: macro.orbit_cash_adj,
    },
    // Final_Action canonical 분류 (A-1/B-1)
    actions: {
      sell_ready:    sellList.map(brief_),
      exit_signals:  exitList.map(brief_),
      buy_signals:   buyList.map(brief_),
      watch_signals: watchList.map(brief_),
      hold_signals:  holdList.map(brief_),
    },
    alerts: {
      stage2_ready:   stage2Pass.map(h=>({ticker:h.Ticker,name:h.Name,profit_pct:h.Profit_Pct,limit_price_est:h.Limit_Price_Est})),
      time_stop_near: timeStopNear.map(h=>({ticker:h.Ticker,name:h.Name,days_left:h.Days_To_Time_Stop,stop_date:h.Time_Stop_Date})),
      overweight:     overweight.map(h=>({ticker:h.Ticker,name:h.Name,weight_pct:h.Weight_Pct})),
      tp1_near:       tp1Near.map(h=>({ticker:h.Ticker,name:h.Name,profit_pct:h.Profit_Pct,tp1_price:h.TP1_Price,tp2_price:h.TP2_Price})),
    },
    upcoming_events: events.upcoming_7d,
    account_snapshot_freshness: acctFresh,
    data_quality: {
      price_stale:  priceStaleList_.map(h=>({ticker:h.Ticker,name:h.Name,price_date:h.Price_Date})),
      quote_only:   quoteOnlyList_.map(h=>({ticker:h.Ticker,name:h.Name})),
      flow_stale:   flowStaleList_.map(h=>({ticker:h.Ticker,name:h.Name,missing_fields:h.Missing_Fields})),
    },
    // sell_priority_engine 출력 (spec: portfolio_exposure.yaml:sell_priority_engine)
    // 활성화: REGIME_TRIM_50 또는 현금 부족. ETF→손실위성→코어주도주 순서로 정렬.
    cash_raise: _cashBelowTgt_ ? {
      active:     true,
      reason:     isRiskOffB ? `REGIME_TRIM_50(${regimeStr})` : "cash_below_target",
      prohibition: "매도수량은 HTS 캡처 제공 후 결정. 수량 미제공 시 수량 기재 금지(spec:P1규칙).",
      sell_priority_table: _cashRaiseCands_,
      sector_exposure_summary: sellPriorityView_.sector_exposure ?? sellPriorityView_.sector_exposure_summary ?? {},
    } : { active: false },
  };
}

// ── E3: 거래 진입 템플릿 생성 ────────────────────────────────────────────────
// BUY_CANDIDATE/WATCH_CANDIDATE 종목에 대해 performance 탭 입력 행 + 진입 체크리스트 반환.
// doGet(?view=trade_template&ticker=064350)
function getTradeTemplate(ticker) {
  if (!ticker) return { error: "ticker 파라미터 필요 (?view=trade_template&ticker=XXXXXX)" };
  const allData = sheetToJson("data_feed");
  const row = allData.find(r => String(r.Ticker) === String(ticker) || r.Name === ticker);
  if (!row) return { error: `ticker ${ticker} not found in data_feed` };

  const macro = getMacroJson();
  const today = Utilities.formatDate(new Date(), "Asia/Seoul", "yyyy-MM-dd");
  const sector = TICKER_SECTOR_MAP[ticker] ?? "N/A";

  // 진입 체크리스트 — 각 항목 true/false
  const checklist = {
    data_quality:    row.Price_Status === "PRICE_OK",
    no_dart_risk:    !row.DART_Risk || row.DART_Risk === "" || row.DART_Risk === "N",
    liquidity_ok:    row.Liquidity_Status === "OK",
    timing_ready:    ["BUY_STAGE1_READY","BUY_PULLBACK_WAIT","BUY_BREAKOUT_PILOT_ONLY"].includes(row.Timing_Action),
    leader_gate:     ["PASS","EXPLORE_CANDIDATE","WATCH_ONLY"].includes(row.Leader_Gate),
    ac_gate:         row.AC_Gate === "CLEAR",
    flow_credit_ok:  parseFloat(row.Flow_Credit) >= 0.4,
    regime_ok:       ["RISK_ON","SECULAR_LEADER_RISK_ON","LEADER_CONCENTRATION"].includes(macro.market_regime),
    heat_ok:         Number.isFinite(parseFloat(macro.total_heat_pct)) && parseFloat(macro.total_heat_pct) < 10,
    fc_budget_ok:    Number.isFinite(parseFloat(macro.fc_budget_pct))  && parseFloat(macro.fc_budget_pct)  < 100,
    nr_feedback_ok:  macro.net_return_feedback !== "REDUCED",
    ee_positive:     parseFloat(row.EE_Est) > 0,
    ss001_grade_ok:  ["A","B"].includes(row.SS001_Grade),
  };
  const passCount  = Object.values(checklist).filter(Boolean).length;
  const totalCheck = Object.keys(checklist).length;
  const gateStatus = passCount === totalCheck ? "ALL_PASS"
                   : passCount >= totalCheck - 2 ? "MINOR_ISSUES"
                   : "BLOCK";

  return {
    ticker,
    name: row.Name,
    sector,
    generated_at: today,
    gate_status: gateStatus,
    gate_score: `${passCount}/${totalCheck}`,
    checklist,
    // performance 탭에 바로 붙여넣을 수 있는 행 템플릿
    performance_tab_template: {
      trade_id:               `${today.replace(/-/g,"")}${ticker}`,
      ticker,
      sector,
      entry_date:             today,
      entry_price:            row.Limit_Price_Est ?? "",
      entry_stage:            "stage_1",
      quantity:               row.Pos_Size_Qty    ?? "",
      stop_price_at_entry:    row.Stop_Price_Est  ?? "",
      target_price_at_entry:  row.Target_Price    ?? "",
      exit_date:              "",
      exit_price:             "",
      exit_reason:            "",
      pnl_pct:                "",
      holding_days:           "",
      entry_c1_score:         row.C1_Price        ?? "",
      entry_c2_score:         row.C2_RelStr       ?? "",
      entry_c3_score:         row.C3_VolSurge     ?? "",
      entry_c4_score:         row.C4_Flow         ?? "",
      entry_c5_score:         row.C5_Sector       ?? "",
      entry_mode:             row.Entry_Mode      ?? "",
      entry_gate:             row.Entry_Mode_Gate ?? "",
      timing_action:          row.Timing_Action   ?? "",
      timing_score_entry:     row.Timing_Score_Entry ?? "",
      timing_score_exit:      row.Timing_Score_Exit  ?? "",
      anti_climax_gate:       row.AC_Gate         ?? "",
      flow_credit:            row.Flow_Credit     ?? "",
      entry_mrs_score:        macro.mrs_score      ?? "",
      fc_bucket:              "",
    },
    current_state: {
      close:             row.Close,
      allowed_action:    row.Allowed_Action,
      timing_action:     row.Timing_Action,
      timing_score_entry: row.Timing_Score_Entry,
      timing_score_exit: row.Timing_Score_Exit,
      timing_block_reason: row.Timing_Block_Reason,
      sell_action:       row.Sell_Action,
      sell_ratio_pct:    row.Sell_Ratio_Pct,
      sell_qty:          row.Sell_Qty,
      sell_limit_price:  row.Sell_Limit_Price,
      sell_price_source: row.Sell_Price_Source,
      sell_reason:       row.Sell_Reason,
      sell_validation:   row.Sell_Validation,
      ss001_grade:       row.SS001_Grade,
      ss001_total:       row.SS001_Total,
      flow_credit:       row.Flow_Credit,
      rw_partial:        row.RW_Partial,
      limit_price_est:   row.Limit_Price_Est,
      stop_price_est:    row.Stop_Price_Est,
      stop_price_source: row.Stop_Price_Source,
      ee_est:            row.EE_Est,
      pos_size_qty:      row.Pos_Size_Qty,
      upside_pct:        row.Upside_Pct,
      atr20:             row.ATR20,
      tp1_price:         row.TP1_Price,
      tp1_qty:           row.TP1_Qty,
      tp2_price:         row.TP2_Price,
      tp2_qty:           row.TP2_Qty,
      dart_risk:         row.DART_Risk,
      days_to_earnings:  row.Days_To_Earnings,
    },
  };
}

function getSummaryJson() {
  // ChatGPT 포트폴리오 분석에 최적화된 통합 뷰
  const sectors  = getSectorFlowJson();
  const port     = getPortfolioJson();
  const macro    = getMacroJson();
  const events   = getEventRiskJson();

  // 포트폴리오 전체 수급 요약
  const holdings = port.holdings;
  const totalFrg5  = holdings.reduce((s,h) => s + (parseFloat(h.Frg_5D) || 0), 0);
  const totalInst5 = holdings.reduce((s,h) => s + (parseFloat(h.Inst_5D) || 0), 0);
  const flowOkCount = holdings.filter(h => h.Flow_OK === "Y").length;

  // SS001 등급 분포 및 Allowed_Action 집계
  const ss001Dist   = { A: 0, B: 0, C: 0, D: 0 };
  const actionDist  = {};
  holdings.forEach(h => {
    const g = h["SS001_Grade"];
    if (g in ss001Dist) ss001Dist[g]++;
    const a = h["Allowed_Action"] || "UNKNOWN";
    actionDist[a] = (actionDist[a] ?? 0) + 1;
  });

  return {
    portfolio_flow_summary: {
      total_holdings: holdings.length,
      data_ok_count: flowOkCount,
      portfolio_frg_5d_total: totalFrg5,
      portfolio_inst_5d_total: totalInst5,
      portfolio_indiv_5d_total: -(totalFrg5 + totalInst5),
    },
    ss001_grade_distribution: ss001Dist,
    action_distribution: actionDist,
    sector_summary: {
      total_sectors: sectors.count,
      top_inflow_sectors: sectors.top_inflow,
      outflow_warning_sectors: sectors.outflow_warning,
      strong_smart_money_sectors: sectors.strong_smart_money,
    },
    macro_snapshot: {
      vix: macro.vix,
      usd_krw: macro.usd_krw,
      kospi: macro.kospi,
      sp500_5d_ret: macro.sp500_ret5d,
      market_regime: macro.market_regime,
      mrs_score: macro.mrs_score,
      bayesian_multiplier: macro.bayesian_multiplier,
      total_heat_pct: macro.total_heat_pct,
      fc_budget_pct: macro.fc_budget_pct,
      net_return_feedback: macro.net_return_feedback,
      orbit_gap_pct:       macro.orbit_gap_pct,
      orbit_state:         macro.orbit_state,
      orbit_slot_adj:      macro.orbit_slot_adj,
      bucket_status:       macro.bucket_status,
      bucket_detail:       macro.bucket_detail,
    },
    event_alerts: events.upcoming_7d,
    holdings_detail: holdings,
    sector_detail: sectors.sectors,
    macro_detail: macro.indicators,
    macro_computed: macro.computed_summary,
  };
}
