447 lines
22 KiB
JavaScript
447 lines
22 KiB
JavaScript
// 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,
|
||
};
|
||
}
|