Files
QuantEngineByItz/gas_report.gs
T

447 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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,
};
}