// factorData.jsx — 리스크 요인 모델 (GIRR/CSR/EQ/FX/VOL) + 채권 요인 데이터(기준금리·스프레드·DV01·Greeks).
// 요인 기여 위험액 = |단위충격 민감도| × 요인 일간표준변동 → CVaR 총액에 스케일 매칭.

const FACTORS = [
  { id: 'rate',   name: '금리',       code: 'GIRR', color: '#0A4FA3', icon: 'percent' },
  { id: 'credit', name: '신용스프레드', code: 'CSR',  color: '#C76A1F', icon: 'scale' },
  { id: 'eq',     name: '주식',       code: 'EQ',   color: '#0E7C5A', icon: 'trendUp' },
  { id: 'fx',     name: '환',         code: 'FX',   color: '#E0922A', icon: 'globe' },
  { id: 'vol',    name: '변동성·개별', code: 'VOL',  color: '#6D5BD0', icon: 'sigma' },
];
const FMAP = {}; FACTORS.forEach(f => FMAP[f.id] = f);

// 요인 일간 표준변동 (금리/신용 bp, 주식/환 %)
const FACTOR_DAILY_MOVE = { rate: 8, credit: 6, eq: 1.5, fx: 0.6 };
// 전일 실제 시장 변동 (MARKET 지표와 정합: UST +3bp, IG OAS +2bp, KOSPI -0.6%, USD/KRW +0.30%)
const FACTOR_MKT_DELTA = { rate: 3, credit: 2, eq: -0.6, fx: 0.30 };

// 단위충격 민감도 (% 손익). rate/credit: +100bp 기준, eq: -10% 기준, fx: +10% 기준.
function factorSens(pf) {
  const zero = { eq: 0, rateBp: 0, creditBp: 0, fxPct: 0, volX: 1 };
  return {
    rate:   simulate(pf, { ...zero, rateBp: 100 }).portPnl,
    credit: simulate(pf, { ...zero, creditBp: 100 }).portPnl,
    eq:     simulate(pf, { ...zero, eq: -10 }).portPnl,
    fx:     simulate(pf, { ...zero, fxPct: 10 }).portPnl,
  };
}

// 요인별 기여 위험액 분해. returns [{id,name,code,color,amt,frac,sensAmt,sensLabel,dod}]
function factorDecompose(pf) {
  const s = factorSens(pf);
  const m = summarize(pf);
  // 일간 표준변동 기준 요인 위험액 (억)
  const raw = {
    rate:   Math.abs(s.rate)   / 100 * FACTOR_DAILY_MOVE.rate,
    credit: Math.abs(s.credit) / 100 * FACTOR_DAILY_MOVE.credit,
    eq:     Math.abs(s.eq)     / 10  * FACTOR_DAILY_MOVE.eq,
    fx:     Math.abs(s.fx)     / 10  * FACTOR_DAILY_MOVE.fx,
  };
  let amt = {}; Object.keys(raw).forEach(k => amt[k] = raw[k] / 100 * pf.value);
  let sum4 = Object.values(amt).reduce((a, b) => a + b, 0) || 1;
  // 스케일 보정: 4요인 합이 CVaR의 90%, 잔차(변동성/개별)가 10%가 되도록
  const k = m.risk * 0.90 / sum4;
  Object.keys(amt).forEach(f => amt[f] *= k);
  amt.vol = m.risk * 0.10;
  // 민감도 금액 (억): 단위충격당 손익
  const sensAmt = {
    rate: s.rate / 100 * pf.value, credit: s.credit / 100 * pf.value,
    eq: s.eq / 100 * pf.value, fx: s.fx / 100 * pf.value, vol: null,
  };
  const sensLabel = {
    rate: '금리 +100bp', credit: '스프레드 +100bp', eq: '주식 −10%', fx: '원화 −10%', vol: '요인 설명 후 잔차',
  };
  return FACTORS.map(f => ({
    ...f, amt: amt[f.id], frac: amt[f.id] / m.risk,
    sensAmt: sensAmt[f.id], sensLabel: sensLabel[f.id],
    // 전일대비: 전일 시장변동 대비 표준변동 비율로 근사
    dod: f.id === 'vol' ? 0 : amt[f.id] * (FACTOR_MKT_DELTA[f.id] / FACTOR_DAILY_MOVE[f.id]) * 0.4,
  })).sort((a, b) => b.amt - a.amt);
}

// 손익요인분해 — 보고서 요약용. 전일 시장 Δ × 민감도 → 요인별 손익 (억).
function pnlExplain(pf) {
  const s = factorSens(pf);
  const pct = {
    rate:   s.rate   * FACTOR_MKT_DELTA.rate   / 100,
    credit: s.credit * FACTOR_MKT_DELTA.credit / 100,
    eq:     s.eq     * (-FACTOR_MKT_DELTA.eq)  / 10,
    fx:     s.fx     * FACTOR_MKT_DELTA.fx     / 10,
  };
  pct.vol = -0.018; // 개별 잔차 (요인 미설명분)
  const items = FACTORS.map(f => ({ ...f, pnl: pct[f.id] / 100 * pf.value, pct: pct[f.id] }));
  const total = items.reduce((a, b) => a + b.pnl, 0);
  const losses = items.filter(i => i.pnl < 0);
  const lossSum = losses.reduce((a, b) => a + b.pnl, 0);
  const top = losses.slice().sort((a, b) => a.pnl - b.pnl)[0];
  const mktText = { rate: `금리 +${FACTOR_MKT_DELTA.rate}bp 상승`, credit: `신용스프레드 +${FACTOR_MKT_DELTA.credit}bp 확대`, eq: `주가 ${FACTOR_MKT_DELTA.eq}% 하락`, fx: `원화 ${FACTOR_MKT_DELTA.fx}% 약세`, vol: '개별 종목 변동' };
  const narration = top && lossSum < 0
    ? `오늘 손실의 ${Math.round(top.pnl / lossSum * 100)}%는 ${mktText[top.id]}에서 발생했어요.`
    : '오늘은 요인 전반에서 유의미한 손실이 발생하지 않았어요.';
  return { items, total, start: pf.value, end: pf.value + total, narration };
}

// 자산군 분류
function assetClassOf(u) {
  if (u.type === '주식') return u.fx ? '해외주식' : '국내주식';
  return u.sector === '해외채권' ? '해외채권' : '국내채권';
}
const ASSET_CLASSES = ['국내채권', '해외채권', '국내주식', '해외주식'];

// 보유 종목의 주 노출 요인 태그 (상위 1~2개)
function mainFactorsOf(id) {
  const u = UMAP[id]; const out = [];
  if (u.type === '주식') {
    // 주식 포트폴리오에서 '주식(eq)' 노출은 자명하므로 표시하지 않고, 구분되는 요인(환·변동성)만 태그
    if (u.fx) out.push('fx');
    if (u.vol >= 30) out.push('vol');
  }
  else {
    out.push('rate');
    if ((u.credit || 0) >= 1) out.push('credit');
    if (u.fx) out.push('fx');
  }
  return out.slice(0, 2);
}

// 전사 합산 포트폴리오 (asset 필터 가능)
function firmPortfolio(asset) {
  const people = asset ? PEOPLE.filter(p => p.asset === asset) : PEOPLE;
  const value = people.reduce((s, m) => s + m.pf.value, 0);
  const limit = people.reduce((s, m) => s + m.pf.limit, 0);
  const agg = {};
  people.forEach(m => m.pf.holds.forEach(h => { agg[h.id] = (agg[h.id] || 0) + h.w * m.pf.value; }));
  const tot = Object.values(agg).reduce((a, b) => a + b, 0) || 1;
  const holds = Object.entries(agg).map(([id, v]) => ({ id, w: +(v / tot * 100).toFixed(2) })).sort((a, b) => b.w - a.w);
  return { holds, value, limit };
}

// ── 그룹별 리스크 기여 (국가/섹터/종목 탑다운) ──
// keyOf(u) 로 묶어 [{key, amt(억), frac, w(비중%)}] 반환. amt 합 = 전체 위험액.
function groupContribs(pf, keyOf) {
  const m = summarize(pf);
  const rc = riskContribs(pf.holds); const fmapc = {}; rc.forEach(c => fmapc[c.id] = c.frac);
  const g = {};
  pf.holds.forEach(h => {
    const u = UMAP[h.id]; const k = keyOf(u);
    if (!g[k]) g[k] = { key: k, amt: 0, w: 0 };
    g[k].amt += (fmapc[h.id] || 0) * m.risk;
    g[k].w += h.w;
  });
  return Object.values(g).map(x => ({ ...x, frac: x.amt / (m.risk || 1) })).sort((a, b) => b.amt - a.amt);
}
// 종목별 기여 [{id,name,...u, amt, frac, w}]
function holdContribs(pf) {
  const m = summarize(pf);
  const rc = riskContribs(pf.holds); const fmapc = {}; rc.forEach(c => fmapc[c.id] = c.frac);
  return pf.holds.map(h => ({ ...UMAP[h.id], w: h.w, amt: (fmapc[h.id] || 0) * m.risk, frac: fmapc[h.id] || 0 }))
    .sort((a, b) => b.amt - a.amt);
}

// ── 채권: 나라별 기준금리 ──
const POLICY_RATES = [
  { country: '한국',   bank: '한국은행',   rate: 2.50, delta: -0.25, asOfMove: '2026.05 인하', seed: 71, mvol: 0.02 },
  { country: '미국',   bank: 'Fed',       rate: 4.50, delta: 0,     asOfMove: '동결 지속',     seed: 13, mvol: 0.015 },
  { country: '유로존', bank: 'ECB',       rate: 2.15, delta: -0.25, asOfMove: '2026.04 인하', seed: 29, mvol: 0.02 },
  { country: '일본',   bank: 'BOJ',       rate: 0.75, delta: 0.25,  asOfMove: '2026.03 인상', seed: 47, mvol: 0.04 },
  { country: '카타르', bank: 'QCB',       rate: 4.65, delta: 0,     asOfMove: '미국 동조',     seed: 59, mvol: 0.012 },
];

// ── 채권: 국가 × 등급 신용스프레드 (bp, 국채 대비 OAS) ──
const SPREAD_GRADES = ['AAA', 'AA', 'A', 'BBB'];
const SPREAD_MATRIX = [
  { country: '한국',   spreads: { 'AAA': 28,  'AA': 45,  'A': 88,  'BBB': 176 }, dod: { 'AAA': 1, 'AA': 1, 'A': 2, 'BBB': 4 } },
  { country: '미국',   spreads: { 'AAA': 34,  'AA': 52,  'A': 92,  'BBB': 148 }, dod: { 'AAA': 1, 'AA': 2, 'A': 2, 'BBB': 3 } },
  { country: '프랑스', spreads: { 'AAA': 41,  'AA': 66,  'A': 108, 'BBB': 172 }, dod: { 'AAA': 0, 'AA': 1, 'A': 3, 'BBB': 5 } },
  { country: '카타르', spreads: { 'AAA': 58,  'AA': 84,  'A': 132, 'BBB': 214 }, dod: { 'AAA': 2, 'AA': 3, 'A': 5, 'BBB': 7 } },
];

// ── 채권: DV01 (금리 1bp 상승 시 평가손, 백만원 단위로 표시 권장) ──
// 포지션 DV01(억) = 평가액 × 비중 × 듀레이션 × 0.0001
function dv01Rows(pf) {
  return pf.holds.map(h => {
    const u = UMAP[h.id]; if (u.type !== '채권') return null;
    const amt = pf.value * h.w / 100;
    return { id: h.id, name: u.name, country: u.country, rating: u.rating, dur: u.dur, w: h.w, amt, dv01: amt * u.dur * 0.0001 };
  }).filter(Boolean).sort((a, b) => b.dv01 - a.dv01);
}
// 헤지 포함 넷 DV01. 헤지 = 국채선물 매도 (그로스의 ~28%)
function dv01Summary(pf) {
  const rows = dv01Rows(pf);
  const gross = rows.reduce((s, r) => s + r.dv01, 0);
  const hedge = -gross * 0.28;
  return { rows, gross, hedge, net: gross + hedge };
}
// 전사(채권) 넷 DV01
function firmDV01() { return dv01Summary(firmPortfolio('채권')); }

// 기간구조(테너) 버킷별 DV01
const TENOR_BUCKETS = [
  { id: 't1', label: '1년 이하',  test: d => d <= 1 },
  { id: 't2', label: '1~3년',    test: d => d > 1 && d <= 3 },
  { id: 't3', label: '3~5년',    test: d => d > 3 && d <= 5 },
  { id: 't5', label: '5~10년',   test: d => d > 5 && d <= 10 },
  { id: 't10', label: '10년 초과', test: d => d > 10 },
];
function tenorDV01(pf) {
  const rows = dv01Rows(pf);
  return TENOR_BUCKETS.map(b => {
    const hit = rows.filter(r => b.test(r.dur));
    return { ...b, dv01: hit.reduce((s, r) => s + r.dv01, 0), n: hit.length };
  });
}

// Greeks — 감마(컨벡시티 기반, 억/(100bp)²), 베가(콜 내재 옵션성, 억/vol 1%p)
function greeksRows(pf) {
  return pf.holds.map(h => {
    const u = UMAP[h.id]; if (u.type !== '채권') return null;
    const amt = pf.value * h.w / 100;
    return { id: h.id, name: u.name, dur: u.dur, call: !!u.call,
      gamma: amt * (u.conv || 0) / 100, vega: u.call ? amt * (u.vega || 0) / 100 : 0 };
  }).filter(Boolean).sort((a, b) => b.gamma - a.gamma);
}

// 요인 시황 지표 (MARKET과 정합 + KOSPI 추가)
const FACTOR_MARKET = [
  { fid: 'rate',   label: '美 국채 10년물', sub: 'UST 10Y',  value: '4.28',    num: 4.28,   unit: '%',  delta: 0.03,  dunit: '%p', ddig: 2, seed: 11, mvol: 0.012, shockText: '금리 +10bp',  shock: (s, v) => s.rate / 10 / 100 * v },
  { fid: 'credit', label: '신용 스프레드', sub: 'IG OAS',   value: '92',      num: 92,     unit: 'bp', delta: 2,     dunit: 'bp', ddig: 0, seed: 23, mvol: 0.030, shockText: 'OAS +10bp',   shock: (s, v) => s.credit / 10 / 100 * v },
  { fid: 'eq',     label: '코스피 지수',   sub: 'KOSPI',    value: '2,748',   num: 2748,   unit: 'pt', delta: -16.6, dunit: 'pt', ddig: 1, seed: 41, mvol: 0.010, shockText: '주식 −1%',    shock: (s, v) => s.eq / 10 / 100 * v },
  { fid: 'fx',     label: '원/달러 환율',  sub: 'USD/KRW',  value: '1,382.5', num: 1382.5, unit: '원', delta: 4.2,   dunit: '원', ddig: 1, seed: 37, mvol: 0.006, shockText: '원화 −1%',    shock: (s, v) => s.fx / 10 / 100 * v },
];

Object.assign(window, {
  FACTORS, FMAP, FACTOR_DAILY_MOVE, FACTOR_MKT_DELTA, FACTOR_MARKET, ASSET_CLASSES,
  factorSens, factorDecompose, pnlExplain, mainFactorsOf, assetClassOf, firmPortfolio,
  groupContribs, holdContribs,
  POLICY_RATES, SPREAD_GRADES, SPREAD_MATRIX, dv01Rows, dv01Summary, firmDV01, TENOR_BUCKETS, tenorDV01, greeksRows,
});
