// 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, }; }