StatPan
  • Home
  • Macro
  • Blog

Macro Dashboard

거시경제 레짐 분석

regime = FileAttachment("../data/macro/regime_current.json").json()
history = FileAttachment("../data/macro/regime_history.json").json()
indicators = FileAttachment("../data/macro/indicators.json").json()
markets = FileAttachment("../data/macro/markets.json").json()
regimeColors = ({
  "Goldilocks": "#2ecc71",
  "과열": "#e67e22",
  "침체/디스인플레": "#3498db",
  "스태그플레이션": "#e74c3c",
  "전환기": "#95a5a6"
})

directionArrow = (d) => ({
  rising: "▲", falling: "▼", stable: "→",
  tightening: "▲ 긴축", holding: "→ 유지", easing: "▼ 완화"
})[d] || ""

fmtNum = (v, d = 1) => v == null ? "—" : Number(v).toFixed(d)

fmtComma = (v) => v == null ? "—" : Number(v).toLocaleString("en-US", {
  minimumFractionDigits: 1, maximumFractionDigits: 1
})
{
  const color = regimeColors[regime.regime_label] || "#95a5a6";
  return html`
    <div style="background:#1a1d2e; border:1px solid #2d3148; border-radius:12px; padding:24px; margin-bottom:8px;">
      <div style="display:flex; align-items:center; gap:16px; flex-wrap:wrap;">
        <span style="background:${color}; color:#000; font-weight:700; font-size:1.3rem;
          padding:6px 20px; border-radius:24px;">${regime.regime_label}</span>
        <span style="color:#8892b0; font-size:0.9rem;">${regime.snapshot_date}</span>
      </div>
      <p style="color:#c8cce0; margin-top:12px; line-height:1.7; font-size:0.95rem;">
        ${regime.regime_summary}
      </p>
    </div>
  `;
}

4축 스코어

{
  const axes = [
    { label: "성장 Growth", score: regime.growth_score, dir: regime.growth_direction,
      color: "#2ecc71", min: 0, max: 100 },
    { label: "물가 Inflation", score: regime.inflation_score, dir: regime.inflation_direction,
      color: "#e67e22", min: 0, max: 100 },
    { label: "통화정책 Policy", score: regime.policy_score, dir: regime.policy_direction,
      color: "#9b59b6", min: -100, max: 100 },
    { label: "리스크 선호 Risk", score: regime.risk_score, dir: null,
      color: "#3498db", min: 0, max: 100 },
  ];

  return html`
    <div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(260px, 1fr)); gap:12px;">
      ${axes.map(a => {
        const pct = ((a.score - a.min) / (a.max - a.min)) * 100;
        const dirText = a.dir ? directionArrow(a.dir) : "";
        return html`
          <div style="background:#1a1d2e; border:1px solid #2d3148; border-radius:8px; padding:16px;">
            <div style="display:flex; justify-content:space-between; margin-bottom:6px;">
              <span style="color:#8892b0; font-size:0.85rem;">${a.label}</span>
              <span style="font-weight:700; font-size:0.95rem;">
                ${a.min < 0 ? (a.score > 0 ? "+" : "") : ""}${fmtNum(a.score)}
                <span style="color:#8892b0; font-size:0.8rem; margin-left:4px;">${dirText}</span>
              </span>
            </div>
            <div style="background:#2d3148; border-radius:4px; height:10px; overflow:hidden;">
              <div style="height:100%; width:${pct}%; background:${a.color}; border-radius:4px;
                transition:width 0.3s;"></div>
            </div>
          </div>
        `;
      })}
    </div>
  `;
}

주요 지표

{
  if (!regime.indicators) return html`<p style="color:#8892b0;">지표 데이터 없음</p>`;

  const ind = regime.indicators;
  const groups = [
    { title: "금리 & 정책", items: [
      ["연준 기준금리", ind.DFF, "%"],
      ["한국 기준금리", ind.KOR_BASE_RATE, "%"],
      ["미 10Y 국채", ind.DGS10, "%"],
      ["장단기 스프레드", ind.T10Y2Y, "%p"],
      ["실질금리 10Y", ind.DFII10, "%"],
      ["기대인플레 5Y", ind.T5YIE, "%"],
      ["NFCI", ind.NFCI, ""],
    ]},
    { title: "리스크 & 변동성", items: [
      ["VIX", ind.VIXCLS, ""],
      ["IG 크레딧 스프레드", ind.BAMLC0A0CMEY, "%"],
      ["HY 크레딧 스프레드", ind.BAMLH0A0HYM2, "%"],
    ]},
    { title: "환율 & 원자재", items: [
      ["원/달러", ind.KRW_X || ind.DEXKOUS_ECOS, "₩"],
      ["달러인덱스", ind.DTWEXBGS, ""],
      ["WTI 원유", ind.DCOILWTICO, "$/bbl"],
      ["브렌트 원유", ind.DCOILBRENTEU, "$/bbl"],
      ["금 선물", ind.GC_F, "$/oz"],
      ["구리 선물", ind.HG_F, "$/lb"],
    ]},
    { title: "실물경제", items: [
      ["중국 제조업 PMI", ind.CHN_MANUFACTURING_PMI, ""],
      ["한국 수출", ind.KOR_EXPORTS_TOTAL, "백만$"],
      ["투자자 예탁금", ind.KOR_INVESTOR_DEPOSIT, "억원"],
    ]},
    { title: "시장", items: [
      ["S&P 500", ind.GSPC, ""],
      ["KOSPI", ind.KS11, ""],
      ["KOSDAQ", ind.KQ11, ""],
      ["MSCI EM", ind.EEM, ""],
    ]},
  ];

  return html`
    <div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(300px, 1fr)); gap:12px;">
      ${groups.map(g => html`
        <div style="background:#1a1d2e; border:1px solid #2d3148; border-radius:8px; padding:16px;">
          <div style="color:#64ffda; font-size:0.8rem; font-weight:600; text-transform:uppercase;
            letter-spacing:0.05em; margin-bottom:10px;">${g.title}</div>
          <table style="width:100%; border-collapse:collapse; font-size:0.85rem;">
            ${g.items.filter(([,v]) => v != null).map(([name, val, unit]) => html`
              <tr style="border-bottom:1px solid #1e2235;">
                <td style="padding:4px 0; color:#c8cce0;">${name}</td>
                <td style="padding:4px 0; text-align:right; font-variant-numeric:tabular-nums;">
                  ${fmtComma(val)} <span style="color:#8892b0; font-size:0.75rem;">${unit}</span>
                </td>
              </tr>
            `)}
          </table>
        </div>
      `)}
    </div>
  `;
}

섹터 리바운드 기회

{
  const sectors = regime.sector_scores;
  if (!sectors || sectors.length === 0)
    return html`<p style="color:#8892b0;">섹터 데이터 없음</p>`;

  return html`
    <div style="background:#1a1d2e; border:1px solid #2d3148; border-radius:8px; padding:16px; overflow-x:auto;">
      <table style="width:100%; border-collapse:collapse; font-size:0.85rem; min-width:600px;">
        <thead>
          <tr style="border-bottom:2px solid #2d3148;">
            <th style="text-align:left; padding:8px; color:#8892b0;">#</th>
            <th style="text-align:left; padding:8px; color:#8892b0;">섹터</th>
            <th style="text-align:right; padding:8px; color:#8892b0;">리바운드</th>
            <th style="text-align:right; padding:8px; color:#8892b0;">방치도</th>
            <th style="text-align:right; padding:8px; color:#8892b0;">환율쿨링</th>
            <th style="text-align:right; padding:8px; color:#8892b0;">20일 초과수익</th>
            <th style="text-align:right; padding:8px; color:#8892b0;">Z-Score</th>
          </tr>
        </thead>
        <tbody>
          ${sectors.map((s, i) => {
            const reboundColor = s.rebound_score >= 70 ? "#2ecc71"
              : s.rebound_score >= 50 ? "#e67e22" : "#e8eaf6";
            return html`
              <tr style="border-bottom:1px solid #1e2235;">
                <td style="padding:6px 8px; color:#8892b0;">${i + 1}</td>
                <td style="padding:6px 8px;">${s.idx_nm}</td>
                <td style="padding:6px 8px; text-align:right; font-weight:700; color:${reboundColor};">
                  ${fmtNum(s.rebound_score)}
                </td>
                <td style="padding:6px 8px; text-align:right;">${fmtNum(s.neglect_score)}</td>
                <td style="padding:6px 8px; text-align:right;">${fmtNum(s.cooling_score)}</td>
                <td style="padding:6px 8px; text-align:right; color:${s.recent_excess_20d >= 0 ? '#2ecc71' : '#e74c3c'};">
                  ${s.recent_excess_20d >= 0 ? "+" : ""}${fmtNum(s.recent_excess_20d, 2)}%
                </td>
                <td style="padding:6px 8px; text-align:right;">${fmtNum(s.zscore, 2)}</td>
              </tr>
            `;
          })}
        </tbody>
      </table>
      <p style="color:#8892b0; font-size:0.75rem; margin-top:10px;">
        리바운드 = 방치도 x 0.6 + 환율쿨링 x 0.4 &nbsp;|&nbsp;
        방치도: 최근 소외 섹터일수록 높음 &nbsp;|&nbsp;
        환율쿨링: 달러 약세 시 아웃퍼폼 경향
      </p>
    </div>
  `;
}

지표 추이

// Group indicator time series by series_id
indicatorSeries = {
  const grouped = {};
  for (const row of indicators) {
    if (!grouped[row.series_id]) grouped[row.series_id] = [];
    grouped[row.series_id].push({
      date: new Date(row.prd_de.slice(0,4) + "-" + row.prd_de.slice(4,6) + "-" + row.prd_de.slice(6,8)),
      value: +row.data_value
    });
  }
  return grouped;
}
seriesLabels = ({
  DFF: "연준 기준금리 (%)",
  T10Y2Y: "장단기 스프레드 (%p)",
  VIXCLS: "VIX",
  DEXKOUS_ECOS: "원/달러 환율",
  DCOILBRENTEU: "브렌트유 ($/bbl)",
  T5YIE: "기대인플레 5Y (%)",
  BAMLH0A0HYM2: "HY 크레딧 스프레드 (%)",
  CPILFESL: "Core CPI (Index)",
  ICSA: "신규 실업수당 청구",
})

selectedSeries = Object.keys(seriesLabels)
viewof pickedSeries = Inputs.select(selectedSeries, {
  label: "지표 선택",
  format: (d) => seriesLabels[d] || d,
  value: "DFF"
})
Plot.plot({
  width: Math.min(800, window.innerWidth - 60),
  height: 280,
  style: { background: "#1a1d2e", color: "#c8cce0", fontSize: "12px" },
  x: { type: "time", label: null },
  y: { label: seriesLabels[pickedSeries] || pickedSeries, grid: true },
  marks: [
    Plot.ruleY([0], { stroke: "#2d3148" }),
    Plot.lineY(indicatorSeries[pickedSeries] || [], {
      x: "date", y: "value", stroke: "#64ffda", strokeWidth: 1.5
    }),
    Plot.dot(indicatorSeries[pickedSeries]?.slice(-1) || [], {
      x: "date", y: "value", fill: "#64ffda", r: 4
    }),
  ]
})

글로벌 지수 추이

marketSeries = {
  const grouped = {};
  for (const row of markets) {
    if (!grouped[row.series_id]) grouped[row.series_id] = [];
    grouped[row.series_id].push({
      date: new Date(row.prd_de.slice(0,4) + "-" + row.prd_de.slice(4,6) + "-" + row.prd_de.slice(6,8)),
      value: +row.data_value
    });
  }
  return grouped;
}

marketLabels = ({
  GSPC: "S&P 500",
  IXIC: "NASDAQ",
  KS11: "KOSPI",
  KQ11: "KOSDAQ",
  N225: "Nikkei 225",
  HSI: "Hang Seng",
  STOXX50E: "Euro Stoxx 50",
  GC_F: "Gold ($/oz)",
  HG_F: "Copper ($/lb)",
  EEM: "MSCI EM ETF",
})
viewof pickedMarket = Inputs.select(Object.keys(marketLabels), {
  label: "지수 선택",
  format: (d) => marketLabels[d] || d,
  value: "KS11"
})
Plot.plot({
  width: Math.min(800, window.innerWidth - 60),
  height: 280,
  style: { background: "#1a1d2e", color: "#c8cce0", fontSize: "12px" },
  x: { type: "time", label: null },
  y: { label: marketLabels[pickedMarket] || pickedMarket, grid: true },
  marks: [
    Plot.lineY(marketSeries[pickedMarket] || [], {
      x: "date", y: "value", stroke: "#e67e22", strokeWidth: 1.5
    }),
    Plot.dot(marketSeries[pickedMarket]?.slice(-1) || [], {
      x: "date", y: "value", fill: "#e67e22", r: 4
    }),
  ]
})

본 대시보드는 자동 생성된 정량 분석이며 투자 권유가 아닙니다. 데이터 갱신: weekly~monthly.

 

© 2026 StatPan. All rights reserved.