"use strict"; const fmt = new Intl.NumberFormat("ko-KR"); function escapeHtml(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function krw(value) { if (value === null || value === undefined || value === "") return "-"; const number = Number(value); if (!Number.isFinite(number)) return "-"; return `${number > 0 ? "+" : ""}${fmt.format(Math.round(number))} KRW`; } function krwAmount(value) { if (value === null || value === undefined || value === "") return "-"; const number = Number(value); if (!Number.isFinite(number)) return "-"; return `${fmt.format(Math.round(number))} KRW`; } function pct(value) { const number = Number(value); if (!Number.isFinite(number)) return "-"; return `${number.toFixed(2)}%`; } function multiple(value) { const number = Number(value); if (!Number.isFinite(number)) return "-"; return `${number.toFixed(2)}x`; } async function readJson(path) { const response = await fetch(path, { headers: { Accept: "application/json" }, cache: "no-store" }); if (!response.ok) throw new Error(`${path} ${response.status}`); return response.json(); } async function readOptionalJson(path) { try { return await readJson(path); } catch (error) { console.warn(`Optional data load failed: ${path}`, error); return null; } } async function readPostsData() { const generatedPosts = await readOptionalJson("data/news-posts.json"); return generatedPosts || readJson("data/posts.json"); } async function readOptionalPostsData() { try { return await readPostsData(); } catch (error) { console.warn("Optional posts data load failed", error); return null; } } function metricCard(label, value, hint = "") { return `
${escapeHtml(label)}${escapeHtml(value)}${hint ? `${escapeHtml(hint)}` : ""}
`; } function countValue(value, suffix = "") { const number = Number(value); if (!Number.isFinite(number)) return "-"; return `${fmt.format(Math.max(0, Math.round(number)))}${suffix}`; } function statusText(value, fallback = "export 대기") { const text = String(value ?? "").trim(); return text || fallback; } function linkButton(link, isPrimary = false) { return `${escapeHtml(link.label)}`; } function postDate(post) { return String(post.date || "").trim(); } function postHref(post) { return String(post.href || "").trim(); } function postTags(post) { const tags = Array.isArray(post.tags) ? post.tags : []; return tags.length ? `
${tags.map((tag) => `${escapeHtml(tag)}`).join("")}
` : ""; } function sortedPosts(posts) { const items = Array.isArray(posts.posts) ? posts.posts.slice() : []; return items.sort((a, b) => postDate(b).localeCompare(postDate(a)) || String(b.featured).localeCompare(String(a.featured))); } function statusBadge(status) { const normalized = String(status || "planned").toLowerCase(); const labelMap = { draft: "작성 중", planned: "예정", planning: "기획 중", writing: "집필 중", preparing: "출간 준비 중", ready: "준비 중", evidence: "검증", published: "공개", operating: "운영", }; return `${escapeHtml(labelMap[normalized] || statusText(status, "예정"))}`; } function evidenceCard(item) { return `
${escapeHtml(item.type || "evidence")}

${escapeHtml(item.title)}

${escapeHtml(item.summary)}

상태
${statusBadge(item.status)}
범위
${escapeHtml(item.scope || "-")}
${escapeHtml(item.link_label || "자료 보기")}
`; } function postItem(post) { const href = postHref(post); const title = href ? `${escapeHtml(post.title)}` : escapeHtml(post.title); const meta = [postDate(post), post.author, post.series].filter(Boolean).join(" · "); return `
${escapeHtml(post.category || "Log")}

${title}

${meta ? `` : ""}

${escapeHtml(post.summary)}

${postTags(post)}
${statusBadge(post.status)}
`; } function strategyCard(strategy) { const href = strategy.href || "strategies.html"; const totalPnl = Number(strategy.total_pnl_krw); const pnlClass = totalPnl < 0 ? "negative" : totalPnl > 0 ? "positive" : "neutral"; return `
${escapeHtml(strategy.rank ? `#${strategy.rank}` : strategy.market || "REPORT")}

${escapeHtml(strategy.name)}

${escapeHtml(strategy.description)}

코드
${escapeHtml(strategy.code)}
마켓
${escapeHtml(strategy.market)}
누적 손익
${escapeHtml(krw(strategy.total_pnl_krw))}
상태
${escapeHtml(statusText(strategy.status))}
자세히 보기
`; } function renderHomeIntro(home) { const title = document.getElementById("homeTitle"); const description = document.getElementById("homeDescription"); const links = document.getElementById("homePrimaryLinks"); const stats = document.getElementById("homeStats"); if (title) title.textContent = home.site_title || "MediaMaak"; if (description) description.textContent = home.description || ""; if (links) { const primaryLinks = Array.isArray(home.primary_links) ? home.primary_links : []; links.innerHTML = primaryLinks.map((link, index) => linkButton(link, index === 0)).join(""); } if (stats) { const metrics = Array.isArray(home.metrics) ? home.metrics : []; stats.innerHTML = metrics.map((metric) => metricCard(metric.label, metric.value, metric.hint)).join(""); } } function renderPublisherBusinessAreas(home) { const target = document.getElementById("publisherBusinessAreas"); if (!target) return; const areas = Array.isArray(home.business_areas) ? home.business_areas : []; target.innerHTML = areas.map((area) => `
${escapeHtml(area.tag || "AREA")}

${escapeHtml(area.title)}

${escapeHtml(area.summary)}

`).join(""); } function renderPublicationList(home) { const target = document.getElementById("publicationList"); if (!target) return; const items = Array.isArray(home.publications) ? home.publications : []; target.innerHTML = items.length ? items.map((item) => `

${escapeHtml(item.title)}

${escapeHtml(item.summary)}

${statusBadge(item.status)}
`).join("") : '
표시할 출판 콘텐츠가 없습니다.
'; } function renderDigitalProducts(home) { const target = document.getElementById("digitalProductList"); if (!target) return; const products = Array.isArray(home.digital_products) ? home.digital_products : []; target.innerHTML = products.length ? products.map((product) => { const actions = Array.isArray(product.actions) ? product.actions : []; return `
DIGITAL PRODUCT

${escapeHtml(product.title)}

${statusBadge(product.status)}

${escapeHtml(product.summary)}

${actions.map((action) => `${escapeHtml(action)}`).join("")}
`; }).join("") : '
표시할 디지털 상품이 없습니다.
'; } function researchCategoryLabel(post) { const category = String(post.category || "").toLowerCase(); const tags = Array.isArray(post.tags) ? post.tags.map((tag) => String(tag).toLowerCase()) : []; if (category.includes("market") || category.includes("economy")) return "경제·기술 자료"; if (tags.some((tag) => tag.includes("codex") || tag === "ai" || tag.includes("machine-learning"))) return "AI·Codex"; if (tags.some((tag) => tag.includes("automation") || tag.includes("data") || tag.includes("excel") || tag.includes("python"))) return "데이터 자동화"; if (tags.some((tag) => tag.includes("backtest") || tag.includes("trading") || tag.includes("upbit") || tag.includes("kospi"))) return "자동매매"; if (category.includes("data")) return "데이터 자동화"; return post.category || "연구 노트"; } function selectResearchNotes(posts) { const items = sortedPosts(posts); const selected = []; const seen = new Set(); items.forEach((post) => { const label = researchCategoryLabel(post); if (selected.length >= 3 || seen.has(label)) return; selected.push({ ...post, research_label: label }); seen.add(label); }); items.forEach((post) => { if (selected.length >= 3) return; if (selected.some((item) => postHref(item) === postHref(post))) return; selected.push({ ...post, research_label: researchCategoryLabel(post) }); }); return selected.slice(0, 3); } function renderResearchNotes(posts) { const target = document.getElementById("researchNotes"); if (!target) return; const items = selectResearchNotes(posts); target.innerHTML = items.length ? items.map((post) => { const href = postHref(post) || "posts.html"; const meta = [post.research_label, postDate(post)].filter(Boolean).join(" · "); return `
${escapeHtml(meta)}

${escapeHtml(post.title)}

${escapeHtml(post.summary)}

${statusBadge(post.status)}
`; }).join("") : '

표시할 연구 노트가 없습니다.

게시글 데이터가 추가되면 이 영역에 표시됩니다.

'; } function renderContact(home) { const target = document.getElementById("contactPanel"); if (!target) return; const contact = home.contact || {}; const items = Array.isArray(contact.items) ? contact.items : []; const href = String(contact.href || "").trim(); const contactLink = href; target.innerHTML = `
CONTACT STATUS

${escapeHtml(contact.status || "문의 채널 준비 중")}

${escapeHtml(contact.summary || "문의 채널을 준비 중입니다.")}

${contactLink ? `문의하기` : '문의 채널 준비 중'}
`; } function renderFeaturedPost(posts) { const target = document.getElementById("featuredPost"); if (!target) return; const items = sortedPosts(posts); const post = items.find((item) => item.featured) || items[0]; if (!post) { target.innerHTML = ""; return; } const href = postHref(post) || "posts.html"; target.innerHTML = `
FEATURED

${escapeHtml(post.title)}

${escapeHtml([post.category, postDate(post)].filter(Boolean).join(" · "))}

${escapeHtml(post.summary)}

${postTags(post)}
`; } function renderLatestPosts(posts) { const target = document.getElementById("latestPosts"); if (!target) return; const items = sortedPosts(posts).slice(0, 3); target.innerHTML = items.length ? items.map((post) => { const href = postHref(post) || "posts.html"; return ` ${escapeHtml([post.category, postDate(post)].filter(Boolean).join(" · "))} ${escapeHtml(post.title)} `; }).join("") : '
표시할 최신 글이 없습니다.
'; } function normalizedPostHref(value) { return String(value || "").replace(/^\/+/, "").trim(); } function postMetricsLookup(metrics) { const items = Array.isArray(metrics?.posts) ? metrics.posts : []; const byKey = new Map(); items.forEach((item) => { const keys = [item.post_id, item.id, normalizedPostHref(item.href)].filter(Boolean); keys.forEach((key) => byKey.set(String(key), item)); }); return byKey; } function postsWithMetrics(posts, metrics) { const lookup = postMetricsLookup(metrics); return sortedPosts(posts).map((post) => { const metric = lookup.get(String(post.id || "")) || lookup.get(normalizedPostHref(post.href)) || {}; return { ...post, view_count: Number(metric.view_count) || 0, unique_count: Number(metric.unique_count) || 0, last_viewed_at: metric.last_viewed_at || "", }; }); } function mobilePostRow(post, index, options = {}) { const href = postHref(post) || "posts.html"; const views = Number(post.view_count) || 0; const meta = [post.category, postDate(post), `조회 ${fmt.format(views)}`].filter(Boolean).join(" · "); const rank = options.rank ? `${escapeHtml(index + 1)}` : ""; return ` ${rank} ${escapeHtml(post.title)} ${escapeHtml(meta)} `; } function renderPopularPosts(posts, metrics) { const target = document.getElementById("popularPosts"); if (!target) return; const items = postsWithMetrics(posts, metrics); const hasViews = items.some((post) => Number(post.view_count) > 0); const sorted = items.sort((a, b) => { if (hasViews) return (Number(b.view_count) || 0) - (Number(a.view_count) || 0) || postDate(b).localeCompare(postDate(a)); return postDate(b).localeCompare(postDate(a)); }).slice(0, 3); target.innerHTML = sorted.length ? sorted.map((post, index) => mobilePostRow(post, index, { rank: true })).join("") : '
표시할 인기 글이 없습니다.
'; } function renderMobileTrendBoard(posts, metrics) { const target = document.getElementById("mobileTrendBoard"); if (!target) return; const items = postsWithMetrics(posts, metrics).slice(0, 6); target.innerHTML = items.length ? items.map((post, index) => mobilePostRow(post, index)).join("") : '
표시할 최신 글이 없습니다.
'; } function latestDailyRow(strategy, kind) { const rows = Array.isArray(strategy.daily) ? strategy.daily.filter((row) => row.type !== "gap") : []; const daily = rows.slice().sort((a, b) => String(b.date || b.label || "").localeCompare(String(a.date || a.label || "")))[0]; if (daily) return { ...daily, source: "daily" }; if (kind !== "actual") return null; const positions = Array.isArray(strategy.open_positions) ? strategy.open_positions : []; if (!positions.length) return null; const openPnl = positions.reduce((sum, position) => sum + (Number(position.unrealized_pnl_krw) || 0), 0); return { date: strategy.summary?.updated_at || "오픈 포지션", pnl_krw: openPnl, cumulative_pnl_krw: strategy.summary?.total_pnl_krw, trade_count: strategy.summary?.open_position_count, source: "open", }; } function renderMobileDailyBoard(data, targetId, kind) { const target = document.getElementById(targetId); if (!target) return; const page = kind === "actual" ? "actual.html" : "simulation.html"; const rows = allPerformanceStrategies(data || {}).map(({ asset, ...strategy }) => { const daily = latestDailyRow(strategy, kind); if (!daily) return null; const pnl = Number(daily.pnl_krw) || 0; const pnlClass = pnl < 0 ? "negative" : pnl > 0 ? "positive" : "neutral"; const date = String(daily.date || "-").split(" ")[0]; const source = daily.source === "open" ? "오픈" : "일별"; const href = `${page}?asset=${encodeURIComponent(asset.code || "")}&strategy=${encodeURIComponent(strategy.code || "")}`; const meta = [asset.label || asset.market, date, `${source} ${fmt.format(Number(daily.trade_count) || 0)}건`].filter(Boolean).join(" · "); return { date, html: ` ${escapeHtml(strategy.name || strategy.code || "전략")} ${escapeHtml(meta)} ${escapeHtml(krw(daily.pnl_krw))} ` }; }).filter(Boolean).sort((a, b) => String(b.date).localeCompare(String(a.date))).slice(0, 5); target.innerHTML = rows.length ? rows.map((row) => row.html).join("") : '
표시할 전략 결과가 없습니다.
'; } function renderTopicLanes(home) { const target = document.getElementById("topicLanes"); if (!target) return; const lanes = Array.isArray(home.topic_lanes) ? home.topic_lanes : []; target.innerHTML = lanes.map((lane) => ` ${escapeHtml(lane.tag || "Topic")} ${escapeHtml(lane.title)}

${escapeHtml(lane.summary)}

`).join(""); } function renderSeries(home) { const target = document.getElementById("seriesGrid"); if (!target) return; const series = Array.isArray(home.series) ? home.series : []; target.innerHTML = series.map((item) => ` ${escapeHtml(item.count || "series")} ${escapeHtml(item.title)}

${escapeHtml(item.summary)}

`).join(""); } function renderBookToc(toc) { const target = document.getElementById("bookToc"); if (!target) return; const chapters = Array.isArray(toc.chapters) ? toc.chapters : []; target.innerHTML = chapters.map((chapter) => `
${escapeHtml(String(chapter.number).padStart(2, "0"))}

${escapeHtml(chapter.title)}

${escapeHtml(chapter.summary)}

${statusBadge(chapter.status)}
`).join(""); } function renderEvidence(evidence) { const target = document.getElementById("evidenceGrid"); if (!target) return; const items = Array.isArray(evidence.items) ? evidence.items : []; target.innerHTML = items.map(evidenceCard).join(""); } function renderPosts(posts) { const target = document.getElementById("postList"); if (!target) return; const items = sortedPosts(posts); target.innerHTML = items.length ? items.map(postItem).join("") : '

표시할 글이 없습니다.

글 데이터가 추가되면 이 영역에 표시됩니다.

'; } function renderPostLoadError(message) { const latest = document.getElementById("latestPosts"); const postList = document.getElementById("postList"); const text = message || "글 데이터를 불러오지 못했습니다."; if (latest) latest.innerHTML = `
${escapeHtml(text)}
`; if (postList) { postList.innerHTML = `

최신 글을 불러오지 못했습니다.

${escapeHtml(text)}

`; } } function renderSystemFlow(home) { const target = document.getElementById("systemFlow"); if (!target) return; const steps = Array.isArray(home.system_flow) ? home.system_flow : []; target.innerHTML = steps.map((step, index) => `
${escapeHtml(String(index + 1).padStart(2, "0"))} ${escapeHtml(step.title)} ${escapeHtml(step.text)}
`).join(""); } function renderDisclosure(home) { const target = document.getElementById("disclosureNotes"); if (!target) return; const notes = Array.isArray(home.disclosure) ? home.disclosure : []; target.innerHTML = notes.map((note) => `
  • ${escapeHtml(note)}
  • `).join(""); } function homeAssetTrail(asset) { const market = String(asset.market || asset.code || "").toUpperCase(); if (market.includes("KOSPI")) return ["KOSPI", "KOSPI200"]; return ["코인", "비트코인"]; } function renderPositionItems(strategy, asset) { const positions = Array.isArray(strategy.open_positions) ? strategy.open_positions : []; if (!positions.length) { const trail = homeAssetTrail(asset); const label = trail[1] || asset.label || strategy.market || "투자 소재"; return `
  • ${escapeHtml(label)} 오픈 포지션 ${fmt.format(Number(strategy.summary?.open_position_count) || 0)}개 · 실현손익 ${krw(strategy.summary?.total_pnl_krw)}
  • `; } return positions.slice(0, 12).map((position) => { const pnl = Number(position.unrealized_pnl_krw); const pnlClass = pnl < 0 ? "negative" : pnl > 0 ? "positive" : "neutral"; return `
  • ${escapeHtml(position.symbol || "-")} ${krw(position.unrealized_pnl_krw)} · ${pct(position.unrealized_return_pct)}
  • `; }).join(""); } function renderVisitorSummary(visitor) { const target = document.getElementById("visitorSummary"); if (!target) return; const home = visitor?.home || {}; const updated = visitor?.updated_at ? `업데이트 ${visitor.updated_at}` : "export 대기"; const cards = [ ["오늘 방문 집계", countValue(home.today_visitors), updated], ["최근 7일 방문 집계", countValue(home.last_7d_visitors), "GA4 home path 기준"], ["최근 30일 방문 집계", countValue(home.last_30d_visitors), "정기 export 집계"], ["누적 페이지 조회", countValue(home.total_pageviews), "개인정보 미포함 공개 집계"], ]; target.innerHTML = cards.map(([label, value, hint]) => metricCard(label, value, hint)).join(""); } function renderHomeLiveStatus(actual) { const target = document.getElementById("homeLiveStatus"); if (!target) return; const assets = getPerformanceAssets(actual || {}); target.innerHTML = assets.map((asset) => { const strategy = asset.strategies[0] || {}; const summary = strategy.summary || {}; const trail = homeAssetTrail(asset); const href = `actual.html?asset=${encodeURIComponent(asset.code || "")}&strategy=${encodeURIComponent(strategy.code || "")}`; const openPnl = Number(summary.open_unrealized_pnl_krw); const openPnlClass = openPnl < 0 ? "negative" : openPnl > 0 ? "positive" : "neutral"; return `
    ${trail.map((item) => `${escapeHtml(item)}`).join("")}

    ${escapeHtml(strategy.name || asset.label || "실거래 전략")}

    ${escapeHtml(strategy.market || asset.market || "-")} · ${escapeHtml(strategy.bar_interval || "-")}

    상세
    실현손익${krw(summary.total_pnl_krw)}
    오픈 포지션${fmt.format(Number(summary.open_position_count) || 0)}개
    미실현 손익${krw(summary.open_unrealized_pnl_krw)}
    `; }).join(""); } async function initHome() { const [home, posts] = await Promise.all([ readOptionalJson("data/home.json"), readOptionalPostsData(), ]); if (home) { renderHomeIntro(home); renderPublisherBusinessAreas(home); renderPublicationList(home); renderDigitalProducts(home); renderContact(home); } if (posts) { renderLatestPosts(posts); renderResearchNotes(posts); } else { renderPostLoadError("연구 노트 데이터를 불러오지 못했습니다."); } } async function initBookPage() { const toc = await readJson("data/book-toc.json"); renderBookToc(toc); } async function initEvidencePage() { const evidence = await readJson("data/evidence-index.json"); renderEvidence(evidence); } async function initPostsPage() { const posts = await readPostsData(); renderPosts(posts); } async function initArchitecturePage() { const home = await readJson("data/home.json"); renderSystemFlow(home); } async function initDisclosurePage() { const home = await readJson("data/home.json"); renderDisclosure(home); } function renderGaps(rows, kind) { const target = document.getElementById("gapMarkers"); if (!target) return; const label = kind === "actual" ? "매매 중단 기간" : "백테스트 중단 기간"; const gaps = rows.filter((row) => row.type === "gap"); target.innerHTML = gaps.length ? gaps.map((row) => `
    ${escapeHtml(row.label || label)} · ${escapeHtml(label)}
    `).join("") : ""; } const DAILY_PNL_PAGE_SIZE = 30; const performanceState = { data: null, assetCode: "", strategyCode: "", dailyPage: 1, resizeHandler: null, }; function legacyPerformanceStrategy(data) { const summary = data.summary || {}; const overview = data.strategy_overview || {}; return { code: overview.code || "legacy-backtest", name: overview.name || "백테스트", market: overview.market || "KRW-BTC", bar_interval: overview.bar_interval || "-", description: overview.description || "", data_sources: Array.isArray(overview.data_sources) ? overview.data_sources : [], rules: Array.isArray(overview.rules) ? overview.rules : [], summary, return_metrics: data.return_metrics || {}, daily: Array.isArray(data.daily) ? data.daily : [], }; } function getPerformanceAssets(data) { if (Array.isArray(data.assets) && data.assets.length) { return data.assets.map((asset) => ({ ...asset, strategies: Array.isArray(asset.strategies) ? asset.strategies : [], })).filter((asset) => asset.strategies.length); } const strategy = legacyPerformanceStrategy(data || {}); return [{ code: "bitcoin_krw", label: "Bitcoin KRW", market: strategy.market, strategies: [strategy], }]; } function allPerformanceStrategies(data) { return getPerformanceAssets(data).flatMap((asset) => asset.strategies.map((strategy) => ({ ...strategy, asset }))); } function selectedPerformanceAsset() { const assets = getPerformanceAssets(performanceState.data || {}); return assets.find((asset) => asset.code === performanceState.assetCode) || assets[0] || { strategies: [] }; } function selectedPerformanceStrategy() { const asset = selectedPerformanceAsset(); return asset.strategies.find((strategy) => { const previousCodes = Array.isArray(strategy.previous_strategy_codes) ? strategy.previous_strategy_codes : []; return strategy.code === performanceState.strategyCode || strategy.source_strategy_code === performanceState.strategyCode || previousCodes.includes(performanceState.strategyCode); }) || asset.strategies[0] || {}; } function updatePerformanceUrl() { const params = new URLSearchParams(window.location.search); if (performanceState.assetCode) params.set("asset", performanceState.assetCode); if (performanceState.strategyCode) params.set("strategy", performanceState.strategyCode); const query = params.toString(); window.history.replaceState(null, "", `${window.location.pathname}${query ? `?${query}` : ""}`); } function populatePerformanceSelectors(data) { const assetSelect = document.getElementById("assetSelect"); const strategySelect = document.getElementById("strategySelect"); if (!assetSelect || !strategySelect) return; const assets = getPerformanceAssets(data); const params = new URLSearchParams(window.location.search); const requestedAsset = params.get("asset"); const requestedStrategy = params.get("strategy"); const defaultAsset = assets.find((asset) => asset.code === requestedAsset) || assets[0]; performanceState.assetCode = performanceState.assetCode || defaultAsset?.code || ""; if (!assets.some((asset) => asset.code === performanceState.assetCode)) { performanceState.assetCode = defaultAsset?.code || ""; } const asset = selectedPerformanceAsset(); const defaultStrategy = asset.strategies.find((strategy) => { const previousCodes = Array.isArray(strategy.previous_strategy_codes) ? strategy.previous_strategy_codes : []; return strategy.code === requestedStrategy || strategy.source_strategy_code === requestedStrategy || previousCodes.includes(requestedStrategy); }) || asset.strategies[0]; performanceState.strategyCode = performanceState.strategyCode || defaultStrategy?.code || ""; if (!asset.strategies.some((strategy) => strategy.code === performanceState.strategyCode)) { performanceState.strategyCode = defaultStrategy?.code || ""; } assetSelect.innerHTML = assets.map((asset) => ` `).join(""); strategySelect.innerHTML = asset.strategies.map((strategy) => ` `).join(""); } function bindPerformanceSelectors() { const assetSelect = document.getElementById("assetSelect"); const strategySelect = document.getElementById("strategySelect"); if (assetSelect) { assetSelect.addEventListener("change", () => { performanceState.assetCode = assetSelect.value; const asset = selectedPerformanceAsset(); performanceState.strategyCode = asset.strategies[0]?.code || ""; performanceState.dailyPage = 1; populatePerformanceSelectors(performanceState.data); updatePerformanceUrl(); renderSelectedPerformance(); }); } if (strategySelect) { strategySelect.addEventListener("change", () => { performanceState.strategyCode = strategySelect.value; performanceState.dailyPage = 1; updatePerformanceUrl(); renderSelectedPerformance(); }); } } function renderDailyPnlRows(rows) { const body = document.getElementById("dailyPnlBody"); const pageInfo = document.getElementById("dailyPnlPageInfo"); const prev = document.getElementById("dailyPnlPrev"); const next = document.getElementById("dailyPnlNext"); if (!body) return; const visibleRows = (Array.isArray(rows) ? rows : []).filter((row) => row.type !== "gap").slice().reverse(); const totalPages = Math.max(1, Math.ceil(visibleRows.length / DAILY_PNL_PAGE_SIZE)); performanceState.dailyPage = Math.min(Math.max(performanceState.dailyPage, 1), totalPages); const start = (performanceState.dailyPage - 1) * DAILY_PNL_PAGE_SIZE; const pageRows = visibleRows.slice(start, start + DAILY_PNL_PAGE_SIZE); body.innerHTML = pageRows.length ? pageRows.map((row) => { const pnl = Number(row.pnl_krw); const pnlClass = pnl < 0 ? "negative" : pnl > 0 ? "positive" : "neutral"; const cumulative = Number(row.cumulative_pnl_krw); const cumulativeClass = cumulative < 0 ? "negative" : cumulative > 0 ? "positive" : "neutral"; return ` ${escapeHtml(row.date || row.label || "-")} ${krw(row.pnl_krw)} ${krw(row.cumulative_pnl_krw)} ${fmt.format(Number(row.trade_count) || 0)} ${fmt.format(Number(row.win_count) || 0)} ${fmt.format(Number(row.loss_count) || 0)} `; }).join("") : '표시할 일별 손익 데이터가 없습니다.'; if (pageInfo) { pageInfo.textContent = `${performanceState.dailyPage} / ${totalPages} · 총 ${fmt.format(visibleRows.length)}일 · 페이지당 ${DAILY_PNL_PAGE_SIZE}행`; } if (prev) prev.disabled = performanceState.dailyPage <= 1; if (next) next.disabled = performanceState.dailyPage >= totalPages; } function performanceChartRows(strategy, kind) { const rows = Array.isArray(strategy.daily) ? strategy.daily : []; const visibleRows = rows.filter((row) => row.type !== "gap"); if (visibleRows.length || kind !== "actual") return rows; const positions = Array.isArray(strategy.open_positions) ? strategy.open_positions : []; let cumulative = 0; return positions.map((position) => { const pnl = Number(position.unrealized_pnl_krw) || 0; cumulative += pnl; return { type: "open_position", date: position.symbol || position.label || "-", label: position.symbol || position.label || "-", pnl_krw: pnl, cumulative_pnl_krw: cumulative, }; }); } function updatePerformanceChartTitle(strategy, kind) { const heading = document.querySelector("#performanceChart")?.closest("section")?.querySelector(".section-heading h2"); if (!heading) return; const rows = Array.isArray(strategy.daily) ? strategy.daily.filter((row) => row.type !== "gap") : []; const positions = Array.isArray(strategy.open_positions) ? strategy.open_positions : []; heading.textContent = kind === "actual" && !rows.length && positions.length ? "오픈 포지션 평가손익 그래프" : "일별 손익 그래프"; } function renderStrategyOverview(strategy, summary) { const target = document.getElementById("strategyOverview"); if (!target) return; const updated = document.getElementById("strategyUpdated"); if (updated) updated.textContent = summary.updated_at || strategy.updated_at || "-"; if (!strategy || !Object.keys(strategy).length) { target.innerHTML = '

    전략 설명 데이터가 없습니다.

    '; return; } const rules = Array.isArray(strategy.rules) ? strategy.rules : []; const dataSources = Array.isArray(strategy.data_sources) ? strategy.data_sources : []; const periodLabel = document.body.dataset.kind === "actual" ? "운영 기간" : "검증 기간"; target.innerHTML = `
    ${escapeHtml(strategy.market || "KRW-BTC")}

    ${escapeHtml(strategy.name || "전략")}

    ${escapeHtml(strategy.description || "")}

    전략 코드
    ${escapeHtml(strategy.code || "-")}
    봉 간격
    ${escapeHtml(strategy.bar_interval || "-")}
    ${escapeHtml(periodLabel)}
    ${escapeHtml(summary.period || "-")}
    데이터
    ${escapeHtml(dataSources.join(", ") || "-")}
    ${rules.length ? `
    ${rules.map((rule) => `
    ${escapeHtml(rule.label)} ${escapeHtml(rule.value)}

    ${escapeHtml(rule.note || "")}

    `).join("")}
    ` : ""} `; } function renderStrategySummary(summary, metrics, asset, totalStrategyCount) { const target = document.getElementById("performanceSummary"); if (!target) return; if ((!summary || !Object.keys(summary).length) && (!metrics || !Object.keys(metrics).length)) { target.innerHTML = '
    전략 요약-
    '; return; } const isActual = document.body.dataset.kind === "actual"; if (isActual) { target.innerHTML = [ metricCard("누적 실현손익", krw(summary.total_pnl_krw), summary.period), metricCard("일평균 실현손익", krw(summary.daily_avg_pnl_krw)), metricCard("승률", pct(summary.win_rate_pct), `${fmt.format(Number(metrics.win_count) || 0)}승 / ${fmt.format(Number(metrics.loss_count) || 0)}패`), metricCard("실현 거래 수", `${fmt.format(Number(summary.trade_count) || 0)}건`), metricCard("오픈 포지션", `${fmt.format(Number(summary.open_position_count) || 0)}개`, `현재 투입 ${krwAmount(summary.open_budget_krw)}`), metricCard("최대 투입 예산", krwAmount(metrics.max_input_budget_krw), "단일 포지션 기준"), metricCard("현재 미실현 손익", krw(metrics.open_unrealized_pnl_krw), pct(metrics.open_unrealized_return_pct)), metricCard("누적 실현수익률", pct(metrics.cumulative_return_pct), "실현손익 / 투입 예산 기준"), metricCard("목표수익률 범위", `${pct(metrics.min_target_profit_pct)} ~ ${pct(metrics.max_target_profit_pct)}`, `평균 ${pct(metrics.avg_target_profit_pct)}`), metricCard("손익비", multiple(metrics.profit_factor), "실현 이익 / 실현 손실"), ].join(""); return; } target.innerHTML = [ metricCard("누적 손익", krw(summary.total_pnl_krw), summary.period), metricCard("일평균 손익", krw(summary.daily_avg_pnl_krw)), metricCard("승률", pct(summary.win_rate_pct)), metricCard("거래 수", `${fmt.format(Number(summary.trade_count) || 0)}건`), metricCard("최대 투입 예산", krwAmount(metrics.max_input_budget_krw), "단일 거래 기준"), metricCard("일 평균 투입 예산", krwAmount(metrics.daily_avg_input_budget_krw), "일별 투입 예산 평균"), metricCard("누적 수익률", pct(metrics.cumulative_return_pct), "누적 손익 / 누적 투입 예산"), metricCard("실현 수익률", pct(metrics.realized_return_pct), "실현손익 / 투입 예산 기준"), metricCard("목표수익률 범위", `${pct(metrics.min_target_profit_pct)} ~ ${pct(metrics.max_target_profit_pct)}`, `평균 ${pct(metrics.avg_target_profit_pct)}`), metricCard("손익비", multiple(metrics.profit_factor), "실현 이익 / 실현 손실"), ].join(""); } function renderActualLinkPanel(strategy) { const target = document.getElementById("actualLinkPanel"); if (!target) return; const available = Boolean(strategy.actual_available); const href = strategy.actual_href || ""; if (!available || !href) { target.innerHTML = ` `; return; } target.innerHTML = ` `; } function bindDailyPagination() { const prev = document.getElementById("dailyPnlPrev"); const next = document.getElementById("dailyPnlNext"); if (prev) { prev.addEventListener("click", () => { performanceState.dailyPage -= 1; renderDailyPnlRows(selectedPerformanceStrategy().daily); }); } if (next) { next.addEventListener("click", () => { performanceState.dailyPage += 1; renderDailyPnlRows(selectedPerformanceStrategy().daily); }); } } function bindGlobalMenu() { const menuItems = Array.from(document.querySelectorAll(".menu-item.has-panel")); if (!menuItems.length) return; const mobileQuery = window.matchMedia("(max-width: 760px)"); const closePanels = (except = null) => { menuItems.forEach((item) => { if (item === except) return; item.classList.remove("is-open"); const link = Array.from(item.children).find((child) => child.classList?.contains("menu-link")); if (link) link.setAttribute("aria-expanded", "false"); }); }; menuItems.forEach((item) => { const link = Array.from(item.children).find((child) => child.classList?.contains("menu-link")); const panel = Array.from(item.children).find((child) => child.classList?.contains("menu-panel")); if (!link || !panel) return; link.setAttribute("aria-haspopup", "true"); link.setAttribute("aria-expanded", "false"); link.addEventListener("click", (event) => { if (!mobileQuery.matches || item.classList.contains("is-open")) return; event.preventDefault(); closePanels(item); item.classList.add("is-open"); link.setAttribute("aria-expanded", "true"); }); }); document.addEventListener("click", (event) => { if (event.target.closest(".global-menu")) return; closePanels(); }); document.addEventListener("keydown", (event) => { if (event.key !== "Escape") return; closePanels(); document.activeElement?.blur?.(); }); mobileQuery.addEventListener?.("change", () => closePanels()); } function renderSelectedPerformance() { const kind = document.body.dataset.kind || "backtest"; const data = performanceState.data || {}; const asset = selectedPerformanceAsset(); const strategy = selectedPerformanceStrategy(); const summary = strategy.summary || {}; const returnMetrics = strategy.return_metrics || {}; const rows = Array.isArray(strategy.daily) ? strategy.daily : []; const chartRows = performanceChartRows(strategy, kind); const strategies = allPerformanceStrategies(data); const title = document.getElementById("performanceTitle"); const intro = document.getElementById("performanceIntro"); if (title) title.textContent = strategy.name || "자동매매 백테스트"; if (intro) { intro.textContent = strategy.description || `${asset.label || asset.market || "선택 투자 소재"} 백테스트의 일별 실현손익을 표시합니다.`; } const updated = document.getElementById("performanceUpdated"); if (updated) updated.textContent = summary.updated_at || "-"; renderStrategySummary(summary, returnMetrics, asset, strategies.length); renderActualLinkPanel(strategy); renderStrategyOverview(strategy, summary); updatePerformanceChartTitle(strategy, kind); renderGaps(rows, kind); renderDailyPnlRows(rows); window.MediaMaakCharts?.renderPerformanceChart(document.getElementById("performanceChart"), chartRows); if (performanceState.resizeHandler) { window.removeEventListener("resize", performanceState.resizeHandler); } performanceState.resizeHandler = () => { const selected = selectedPerformanceStrategy(); window.MediaMaakCharts?.renderPerformanceChart(document.getElementById("performanceChart"), performanceChartRows(selected, document.body.dataset.kind || "backtest")); }; window.addEventListener("resize", performanceState.resizeHandler); } async function initPerformance() { const source = document.body.dataset.source; const data = await readJson(source); performanceState.data = data; populatePerformanceSelectors(data); bindPerformanceSelectors(); bindDailyPagination(); updatePerformanceUrl(); renderSelectedPerformance(); } async function initStrategies() { const data = await readJson("data/strategies.json"); const target = document.getElementById("strategyList"); if (target) target.innerHTML = data.strategies.map(strategyCard).join(""); } async function init() { bindGlobalMenu(); const page = document.body.dataset.page; try { if (page === "home") await initHome(); if (page === "book") await initBookPage(); if (page === "evidence") await initEvidencePage(); if (page === "posts") await initPostsPage(); if (page === "architecture") await initArchitecturePage(); if (page === "disclosure") await initDisclosurePage(); if (page === "performance") await initPerformance(); if (page === "strategies") await initStrategies(); } catch (error) { const main = document.querySelector("main"); if (main) { main.insertAdjacentHTML("afterbegin", `
    데이터 로드 오류${escapeHtml(error.message)}
    `); } } } document.addEventListener("DOMContentLoaded", init);