株価分析ツール「Stock Chart」



画像

・ダウンロードされる方はこちら。↓

・ソースコードはこちら。↓

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>株価傾向分析ツール</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@300;400;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
<style>
  :root {
    --bg: #0a0c10;
    --bg2: #111418;
    --bg3: #1a1f28;
    --border: #242c3a;
    --accent: #00e5ff;
    --accent2: #ff6b35;
    --accent3: #7fff6b;
    --text: #c8d8e8;
    --text2: #6a8090;
    --up: #00e676;
    --down: #ff1744;
    --warn: #ffea00;
    --panel: rgba(26,31,40,0.9);
  }
  * { box-sizing: border-box; margin: 0; padding: 0; }
  body {
    background: var(--bg);
    color: var(--text);
    font-family: 'Noto Sans JP', sans-serif;
    font-size: 13px;
    min-height: 100vh;
    overflow-x: hidden;
  }
  body::before {
    content: '';
    position: fixed;
    top: 0; left: 0; right: 0; bottom: 0;
    background: 
      radial-gradient(ellipse at 20% 20%, rgba(0,229,255,0.04) 0%, transparent 60%),
      radial-gradient(ellipse at 80% 80%, rgba(255,107,53,0.04) 0%, transparent 60%);
    pointer-events: none;
    z-index: 0;
  }

  header {
    padding: 16px 24px;
    border-bottom: 1px solid var(--border);
    display: flex;
    align-items: center;
    gap: 16px;
    background: rgba(10,12,16,0.95);
    position: sticky; top: 0; z-index: 100;
    backdrop-filter: blur(10px);
  }
  .logo {
    font-family: 'Space Mono', monospace;
    font-size: 18px;
    color: var(--accent);
    letter-spacing: 2px;
    white-space: nowrap;
  }
  .logo span { color: var(--accent2); }
  .status-bar {
    flex: 1;
    display: flex;
    gap: 20px;
    align-items: center;
    font-family: 'Space Mono', monospace;
    font-size: 11px;
    color: var(--text2);
    overflow: hidden;
  }
  .stat-pill {
    display: flex;
    align-items: center;
    gap: 6px;
    white-space: nowrap;
  }
  .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }
  .dot.warn { background: var(--warn); }
  .dot.ok { background: var(--up); }

  .main-layout {
    display: grid;
    grid-template-columns: 280px 1fr;
    grid-template-rows: auto 1fr;
    gap: 0;
    min-height: calc(100vh - 56px);
    position: relative;
    z-index: 1;
  }

  .sidebar {
    grid-row: 1 / 3;
    border-right: 1px solid var(--border);
    background: var(--bg2);
    padding: 16px;
    display: flex;
    flex-direction: column;
    gap: 16px;
    overflow-y: auto;
  }

  .content-area {
    display: flex;
    flex-direction: column;
    gap: 0;
    overflow: hidden;
  }

  .section-title {
    font-family: 'Space Mono', monospace;
    font-size: 10px;
    letter-spacing: 2px;
    color: var(--text2);
    text-transform: uppercase;
    margin-bottom: 8px;
    padding-bottom: 6px;
    border-bottom: 1px solid var(--border);
  }

  .panel {
    background: var(--bg3);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 14px;
  }

  label { display: block; color: var(--text2); font-size: 11px; margin-bottom: 4px; }
  input, select {
    width: 100%;
    background: var(--bg);
    border: 1px solid var(--border);
    color: var(--text);
    padding: 7px 10px;
    border-radius: 4px;
    font-size: 12px;
    font-family: inherit;
    outline: none;
    transition: border-color 0.2s;
  }
  input:focus, select:focus { border-color: var(--accent); }
  input[type="range"] {
    padding: 4px 0;
    border: none;
    background: transparent;
    accent-color: var(--accent);
  }

  .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 8px 16px;
    border-radius: 4px;
    font-size: 12px;
    font-family: 'Space Mono', monospace;
    cursor: pointer;
    border: none;
    transition: all 0.2s;
    width: 100%;
  }
  .btn-primary {
    background: var(--accent);
    color: #000;
    font-weight: 700;
  }
  .btn-primary:hover { background: #33eeff; box-shadow: 0 0 20px rgba(0,229,255,0.3); }
  .btn-secondary {
    background: transparent;
    color: var(--accent2);
    border: 1px solid var(--accent2);
  }
  .btn-secondary:hover { background: rgba(255,107,53,0.1); }
  .btn-sm { padding: 5px 10px; width: auto; font-size: 11px; }
  .btn:disabled { opacity: 0.4; cursor: not-allowed; }

  .file-drop {
    border: 2px dashed var(--border);
    border-radius: 6px;
    padding: 20px;
    text-align: center;
    cursor: pointer;
    transition: all 0.2s;
    color: var(--text2);
    font-size: 12px;
  }
  .file-drop:hover, .file-drop.drag { border-color: var(--accent); color: var(--accent); }
  .file-drop input { display: none; }

  .tab-bar {
    display: flex;
    border-bottom: 1px solid var(--border);
    background: var(--bg2);
    padding: 0 16px;
  }
  .tab {
    padding: 12px 20px;
    font-family: 'Space Mono', monospace;
    font-size: 11px;
    letter-spacing: 1px;
    cursor: pointer;
    color: var(--text2);
    border-bottom: 2px solid transparent;
    transition: all 0.2s;
    white-space: nowrap;
  }
  .tab.active { color: var(--accent); border-bottom-color: var(--accent); }
  .tab:hover { color: var(--text); }

  .tab-content {
    display: none;
    padding: 16px;
    flex: 1;
    overflow-y: auto;
  }
  .tab-content.active { display: block; }

  .chart-wrap {
    background: var(--bg3);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 16px;
    margin-bottom: 16px;
    position: relative;
  }
  .chart-title {
    font-family: 'Space Mono', monospace;
    font-size: 11px;
    letter-spacing: 1px;
    color: var(--text2);
    margin-bottom: 12px;
  }
  .chart-wrap canvas { max-height: 260px; }

  .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
  .grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; }

  .kpi-card {
    background: var(--bg3);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 14px;
  }
  .kpi-label { font-size: 10px; color: var(--text2); letter-spacing: 1px; text-transform: uppercase; margin-bottom: 6px; }
  .kpi-value { font-family: 'Space Mono', monospace; font-size: 22px; font-weight: 700; }
  .kpi-value.up { color: var(--up); }
  .kpi-value.down { color: var(--down); }
  .kpi-value.neutral { color: var(--accent); }
  .kpi-sub { font-size: 10px; color: var(--text2); margin-top: 4px; }

  .trend-badge {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 3px 10px;
    border-radius: 12px;
    font-size: 11px;
    font-family: 'Space Mono', monospace;
    font-weight: 700;
  }
  .trend-bull { background: rgba(0,230,118,0.15); color: var(--up); border: 1px solid var(--up); }
  .trend-bear { background: rgba(255,23,68,0.15); color: var(--down); border: 1px solid var(--down); }
  .trend-neutral { background: rgba(255,234,0,0.1); color: var(--warn); border: 1px solid var(--warn); }

  .prob-bar-wrap { margin-bottom: 8px; }
  .prob-bar-label { display: flex; justify-content: space-between; font-size: 11px; color: var(--text2); margin-bottom: 3px; }
  .prob-bar-bg { background: var(--bg); border-radius: 2px; height: 8px; overflow: hidden; }
  .prob-bar-fill { height: 100%; border-radius: 2px; transition: width 0.6s ease; }

  .vol-indicator {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 12px;
    background: var(--bg);
    border-radius: 4px;
    margin-bottom: 8px;
    border-left: 3px solid var(--accent);
  }
  .vol-indicator.high { border-left-color: var(--down); }
  .vol-indicator.low { border-left-color: var(--up); }
  .vol-indicator.med { border-left-color: var(--warn); }

  .dist-table { width: 100%; border-collapse: collapse; font-size: 12px; }
  .dist-table th {
    font-family: 'Space Mono', monospace;
    font-size: 10px;
    letter-spacing: 1px;
    color: var(--text2);
    text-align: left;
    padding: 8px;
    border-bottom: 1px solid var(--border);
    white-space: nowrap;
  }
  .dist-table td {
    padding: 7px 8px;
    border-bottom: 1px solid rgba(36,44,58,0.5);
    font-family: 'Space Mono', monospace;
    font-size: 11px;
  }
  .dist-table tr:hover td { background: rgba(0,229,255,0.04); }

  #loading {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(10,12,16,0.8);
    z-index: 999;
    align-items: center;
    justify-content: center;
    flex-direction: column;
    gap: 16px;
    backdrop-filter: blur(4px);
  }
  #loading.show { display: flex; }
  .spinner {
    width: 40px; height: 40px;
    border: 3px solid var(--border);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
  }
  @keyframes spin { to { transform: rotate(360deg); } }

  #log {
    font-family: 'Space Mono', monospace;
    font-size: 10px;
    color: var(--text2);
    max-height: 80px;
    overflow-y: auto;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 4px;
    padding: 8px;
  }
  #log div { margin-bottom: 2px; }
  #log .ok { color: var(--up); }
  #log .err { color: var(--down); }
  #log .info { color: var(--accent); }

  .no-data {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 200px;
    color: var(--text2);
    font-size: 12px;
    gap: 8px;
  }
  .no-data-icon { font-size: 36px; opacity: 0.3; }

  .range-row { display: flex; gap: 8px; align-items: flex-end; }
  .range-row > * { flex: 1; }

  @media (max-width: 768px) {
    .main-layout { grid-template-columns: 1fr; }
    .sidebar { grid-row: auto; border-right: none; border-bottom: 1px solid var(--border); }
    .grid-2, .grid-3 { grid-template-columns: 1fr; }
  }
</style>
</head>
<body>

<div id="loading"><div class="spinner"></div><div style="font-family:'Space Mono',monospace;font-size:12px;color:var(--accent)">データ取得中...</div></div>

<header>
  <div class="logo">QUANT<span>VIEW</span></div>
  <div class="status-bar">
    <div class="stat-pill"><div class="dot" id="dot-data"></div><span id="stat-symbol">─ データ未読み込み</span></div>
    <div class="stat-pill"><div class="dot ok"></div><span id="stat-range">範囲: ─</span></div>
    <div class="stat-pill"><div class="dot warn" id="dot-vol"></div><span id="stat-vol">ボラ: ─</span></div>
  </div>
</header>

<div class="main-layout">
  <!-- SIDEBAR -->
  <div class="sidebar">
    <div>
      <div class="section-title">データソース</div>
      <div class="panel" style="margin-bottom:10px">
        <label>銘柄シンボル (Stooq / Yahoo Finance)</label>
        <input type="text" id="symbolInput" placeholder="例: 7203.T, AAPL, BTC-USD" value="7203.T">
        <div style="font-size:10px;color:var(--text2);margin-top:3px;line-height:1.7">
          日本株: <span style="color:var(--accent)">7203.T</span> &nbsp;
          米株: <span style="color:var(--accent)">AAPL</span> &nbsp;
          BTC: <span style="color:var(--accent)">BTC-USD</span><br>
          日経225: <span style="color:var(--accent)">^N225</span> &nbsp;
          S&amp;P500: <span style="color:var(--accent)">^GSPC</span>
        </div>
        <div style="margin-top:6px; display:flex; gap:6px">
          <select id="periodSelect" style="flex:1">
            <option value="1mo">1ヶ月</option>
            <option value="3mo">3ヶ月</option>
            <option value="6mo">6ヶ月</option>
            <option value="1y" selected>1年</option>
            <option value="2y">2年</option>
            <option value="5y">5年</option>
          </select>
          <select id="intervalSelect" style="flex:1">
            <option value="1d" selected>日足</option>
            <option value="1wk">週足</option>
            <option value="1mo">月足</option>
          </select>
        </div>
        <button class="btn btn-primary" style="margin-top:8px" onclick="fetchFromAPI()">▶ WEBから取得 (Stooq優先)</button>
      </div>

      <div class="file-drop panel" id="dropZone" onclick="document.getElementById('csvFile').click()" ondragover="ev(event)" ondrop="dropFile(event)">
        <input type="file" id="csvFile" accept=".csv" onchange="loadCSV(this)">
        <div style="font-size:20px;margin-bottom:6px">📂</div>
        <div>CSVファイルをドロップ</div>
        <div style="font-size:10px;margin-top:4px;color:var(--text2)">または クリックして選択</div>
        <div style="font-size:10px;margin-top:6px;color:var(--text2);line-height:1.6">
          CSV入手先:<br>
          ・<a href="https://stooq.com" target="_blank" style="color:var(--accent)">stooq.com</a> (日本株/米株)<br>
          ・Yahoo Finance → 履歴データ
        </div>
      </div>
    </div>

    <div>
      <div class="section-title">集計範囲</div>
      <div class="panel">
        <div class="range-row" style="margin-bottom:8px">
          <div><label>開始日</label><input type="date" id="rangeStart"></div>
          <div><label>終了日</label><input type="date" id="rangeEnd"></div>
        </div>
        <label>最新 N日間のみ</label>
        <div style="display:flex;gap:8px;align-items:center">
          <input type="range" id="nDays" min="10" max="500" value="252" oninput="document.getElementById('nDaysVal').textContent=this.value">
          <span id="nDaysVal" style="font-family:'Space Mono',monospace;font-size:12px;color:var(--accent);min-width:36px">252</span>
        </div>
        <button class="btn btn-secondary" style="margin-top:8px" onclick="applyRange()">範囲を適用</button>
      </div>
    </div>

    <div>
      <div class="section-title">分析設定</div>
      <div class="panel">
        <label>移動平均 (短期)</label>
        <input type="number" id="maShort" value="5" min="2" max="50">
        <label style="margin-top:8px">移動平均 (長期)</label>
        <input type="number" id="maLong" value="25" min="5" max="200">
        <label style="margin-top:8px">ボラティリティ窓 (日)</label>
        <input type="number" id="volWindow" value="20" min="5" max="60">
        <label style="margin-top:8px">価格ビン幅 (%)</label>
        <input type="number" id="binSize" value="1" min="0.1" max="5" step="0.1">
        <button class="btn btn-primary" style="margin-top:10px" onclick="runAnalysis()">▶ 分析実行</button>
      </div>
    </div>

    <div>
      <div class="section-title">ログ</div>
      <div id="log"></div>
    </div>
  </div>

  <!-- CONTENT -->
  <div class="content-area">
    <div class="tab-bar">
      <div class="tab active" onclick="switchTab('overview')">概要</div>
      <div class="tab" onclick="switchTab('price')">価格チャート</div>
      <div class="tab" onclick="switchTab('volatility')">ボラティリティ</div>
      <div class="tab" onclick="switchTab('distribution')">分布分析</div>
      <div class="tab" onclick="switchTab('probability')">確率マトリクス</div>
    </div>

    <!-- OVERVIEW -->
    <div class="tab-content active" id="tab-overview">
      <div id="kpi-area" class="grid-3" style="margin-bottom:16px"></div>
      <div class="grid-2">
        <div class="chart-wrap">
          <div class="chart-title">価格推移 (概要)</div>
          <canvas id="chart-overview-price"></canvas>
        </div>
        <div class="chart-wrap">
          <div class="chart-title">日次リターン分布</div>
          <canvas id="chart-overview-dist"></canvas>
        </div>
      </div>
      <div id="trend-summary" class="panel" style="margin-top:0"></div>
    </div>

    <!-- PRICE -->
    <div class="tab-content" id="tab-price">
      <div class="chart-wrap" style="margin-bottom:16px">
        <div class="chart-title">価格チャート + 移動平均</div>
        <canvas id="chart-price" style="max-height:320px"></canvas>
      </div>
      <div class="chart-wrap">
        <div class="chart-title">出来高</div>
        <canvas id="chart-volume"></canvas>
      </div>
    </div>

    <!-- VOLATILITY -->
    <div class="tab-content" id="tab-volatility">
      <div class="chart-wrap" style="margin-bottom:16px">
        <div class="chart-title">ローリングボラティリティ (年率換算 %)</div>
        <canvas id="chart-vol"></canvas>
      </div>
      <div class="grid-2">
        <div>
          <div class="section-title" style="margin-bottom:10px">ボラティリティ分類</div>
          <div id="vol-classification"></div>
        </div>
        <div class="chart-wrap">
          <div class="chart-title">ボラ水準別 翌日リターン分布</div>
          <canvas id="chart-vol-return"></canvas>
        </div>
      </div>
    </div>

    <!-- DISTRIBUTION -->
    <div class="tab-content" id="tab-distribution">
      <div class="grid-2" style="margin-bottom:16px">
        <div class="chart-wrap">
          <div class="chart-title">価格変化率ヒストグラム (日次%)</div>
          <canvas id="chart-hist"></canvas>
        </div>
        <div class="chart-wrap">
          <div class="chart-title">自己相関 (ACF)</div>
          <canvas id="chart-acf"></canvas>
        </div>
      </div>
      <div class="chart-wrap">
        <div class="chart-title">時間的分布 (月別 平均リターン)</div>
        <canvas id="chart-monthly"></canvas>
      </div>
    </div>

    <!-- PROBABILITY -->
    <div class="tab-content" id="tab-probability">
      <div class="panel" style="margin-bottom:16px">
        <div class="section-title" style="margin-bottom:10px">近似値出現確率テーブル</div>
        <div style="display:flex;gap:12px;margin-bottom:12px;align-items:flex-end;flex-wrap:wrap">
          <div>
            <label>基準価格 (自動=現在値)</label>
            <input type="number" id="refPrice" placeholder="自動" style="width:140px">
          </div>
          <div>
            <label>許容範囲 ±%</label>
            <input type="number" id="tolerance" value="2" min="0.1" max="20" step="0.1" style="width:100px">
          </div>
          <button class="btn btn-secondary btn-sm" onclick="buildProbTable()">計算</button>
        </div>
        <div id="prob-table-area"></div>
      </div>
      <div class="chart-wrap">
        <div class="chart-title">確率密度推定 (KDE)</div>
        <canvas id="chart-kde"></canvas>
      </div>
    </div>
  </div>
</div>

<script>
// ─── State ────────────────────────────────────────────────
let rawData = [];       // {date, open, high, low, close, volume}
let filteredData = [];
let charts = {};

// ─── Utils ────────────────────────────────────────────────
function log(msg, type='info') {
  const el = document.getElementById('log');
  const d = document.createElement('div');
  d.className = type;
  d.textContent = `> ${msg}`;
  el.appendChild(d);
  el.scrollTop = el.scrollHeight;
}
function showLoading(v) {
  document.getElementById('loading').classList.toggle('show', v);
}
function ev(e) { e.preventDefault(); document.getElementById('dropZone').classList.add('drag'); }
function fmtN(n, dec=2) { return isNaN(n) ? '─' : n.toFixed(dec); }
function fmtPct(n) { return isNaN(n) ? '─' : (n >= 0 ? '+' : '') + n.toFixed(2) + '%'; }
function avg(arr) { return arr.reduce((a,b)=>a+b,0)/arr.length; }
function std(arr) { const m=avg(arr); return Math.sqrt(arr.reduce((a,b)=>a+(b-m)**2,0)/arr.length); }

function ma(arr, n) {
  return arr.map((_,i) => i < n-1 ? null : avg(arr.slice(i-n+1, i+1)));
}

function destroyChart(id) {
  if (charts[id]) { charts[id].destroy(); delete charts[id]; }
}

// ─── API Fetch (multi-proxy fallback + Stooq CSV) ─────────
async function fetchFromAPI() {
  const sym = document.getElementById('symbolInput').value.trim().toUpperCase();
  const period = document.getElementById('periodSelect').value;
  const interval = document.getElementById('intervalSelect').value;
  if (!sym) { log('シンボルを入力してください', 'err'); return; }
  showLoading(true);
  log(`取得中: ${sym} ...`, 'info');

  // ── 手法1: Stooq CSV (日本株・米株広くカバー、CORS制限なし) ──
  const stooqSym = toStooqSymbol(sym);
  try {
    const data = await fetchStooq(stooqSym, period);
    if (data && data.length > 5) {
      rawData = data;
      log(`Stooq取得完了: ${rawData.length}件`, 'ok');
      document.getElementById('stat-symbol').textContent = `${sym} (${rawData.length}件)`;
      initRange(); applyRange();
      showLoading(false);
      return;
    }
  } catch(e) { log(`Stooq失敗: ${e.message}`, 'info'); }

  // ── 手法2: Yahoo Finance v8 (複数プロキシ順次試行) ──
  const yahooUrl = `https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(sym)}?range=${period}&interval=${interval}&includePrePost=false`;
  const proxies = [
    u => `https://corsproxy.io/?${encodeURIComponent(u)}`,
    u => `https://proxy.cors.sh/${u}`,
    u => `https://thingproxy.freeboard.io/fetch/${u}`,
    u => `https://cors-anywhere.herokuapp.com/${u}`,
  ];

  for (const makeProxy of proxies) {
    try {
      const proxyUrl = makeProxy(yahooUrl);
      log(`プロキシ試行中...`, 'info');
      const res = await fetchWithTimeout(proxyUrl, 6000);
      if (!res.ok) continue;
      const text = await res.text();
      const data = JSON.parse(text);
      const result = data.chart?.result?.[0];
      if (!result) continue;
      const ts = result.timestamp;
      const q = result.indicators.quote[0];
      rawData = ts.map((t,i) => ({
        date: new Date(t*1000).toISOString().split('T')[0],
        open: q.open[i], high: q.high[i], low: q.low[i],
        close: q.close[i], volume: q.volume[i]
      })).filter(d => d.close != null);
      log(`Yahoo Finance取得完了: ${rawData.length}件`, 'ok');
      document.getElementById('stat-symbol').textContent = `${sym} (${rawData.length}件)`;
      initRange(); applyRange();
      showLoading(false);
      return;
    } catch(e) { /* next proxy */ }
  }

  log('全プロキシ失敗。CSVをご利用ください', 'err');
  showLoadingError();
  showLoading(false);
}

function fetchWithTimeout(url, ms) {
  return Promise.race([
    fetch(url, { mode: 'cors' }),
    new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), ms))
  ]);
}

// Stooq シンボル変換 (7203.T7203.jp, AAPL → aapl.us, BTC-USD → btc.v)
function toStooqSymbol(sym) {
  if (/^[\d]{4}\.T$/.test(sym)) return sym.replace('.T', '.jp').toLowerCase();
  if (/^[\d]{4}$/.test(sym)) return sym + '.jp';
  if (/BTC|ETH|XRP/i.test(sym)) return sym.replace(/-USD$/i, '').toLowerCase() + '.v';
  if (/\^N225/.test(sym)) return '^nkx';
  if (/\^DJI/.test(sym)) return '^dji';
  if (/\^GSPC/.test(sym)) return '^spx';
  return sym.toLowerCase() + '.us';
}

// Stooq CSV 取得
async function fetchStooq(sym, period) {
  const daysMap = { '1mo':30,'3mo':90,'6mo':180,'1y':365,'2y':730,'5y':1825 };
  const days = daysMap[period] || 365;
  const endDate = new Date();
  const startDate = new Date(); startDate.setDate(endDate.getDate() - days);
  const fmt = d => d.toISOString().split('T')[0].replace(/-/g,'');
  const url = `https://stooq.com/q/d/l/?s=${encodeURIComponent(sym)}&d1=${fmt(startDate)}&d2=${fmt(endDate)}&i=d`;
  log(`Stooq: ${url}`, 'info');
  const res = await fetchWithTimeout(url, 8000);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const text = await res.text();
  if (text.includes('No data') || text.length < 50) throw new Error('データなし');
  const lines = text.trim().split('\n');
  const header = lines[0].toLowerCase().split(',');
  const di = header.findIndex(h=>h.includes('date'));
  const oi = header.findIndex(h=>h.includes('open'));
  const hi = header.findIndex(h=>h.includes('high'));
  const li = header.findIndex(h=>h.includes('low'));
  const ci = header.findIndex(h=>h.includes('close'));
  const vi = header.findIndex(h=>h.includes('vol'));
  if (ci < 0) throw new Error('Close列なし');
  return lines.slice(1).map(l => {
    const c = l.split(',');
    return { date: c[di]||'', open: parseFloat(c[oi]), high: parseFloat(c[hi]), low: parseFloat(c[li]), close: parseFloat(c[ci]), volume: vi>=0 ? parseFloat(c[vi]) : 0 };
  }).filter(d => !isNaN(d.close) && d.date);
}

function showLoadingError() {
  document.getElementById('stat-symbol').textContent = '取得失敗 ─ CSVをご利用ください';
  // Show helper in log
  log('─── CSVの用意方法 ───', 'info');
  log('Yahoo Finance → 銘柄ページ → 履歴データ → ダウンロード', 'info');
  log('Stooq.com → 銘柄 → Dataタブ → CSVダウンロード', 'info');
}

// ─── CSV Load ─────────────────────────────────────────────
function dropFile(e) {
  e.preventDefault();
  document.getElementById('dropZone').classList.remove('drag');
  const file = e.dataTransfer.files[0];
  if (file) parseCSVFile(file);
}
function loadCSV(input) {
  if (input.files[0]) parseCSVFile(input.files[0]);
}
function parseCSVFile(file) {
  const reader = new FileReader();
  reader.onload = e => {
    try {
      const lines = e.target.result.trim().split('\n');
      const header = lines[0].split(',').map(h => h.trim().toLowerCase().replace(/[""]/g,''));
      log(`CSV列: ${header.join(', ')}`, 'info');

      const idx = {
        date: header.findIndex(h => /date|time|日付/.test(h)),
        open: header.findIndex(h => /open|始値/.test(h)),
        high: header.findIndex(h => /high|高値/.test(h)),
        low: header.findIndex(h => /low|安値/.test(h)),
        close: header.findIndex(h => /close|終値|adj/.test(h)),
        volume: header.findIndex(h => /vol|出来高/.test(h)),
      };
      if (idx.close < 0) throw new Error('Close列が見つかりません');
      if (idx.date < 0) idx.date = 0;

      rawData = lines.slice(1).map(line => {
        const c = line.split(',').map(v => v.trim().replace(/[""]/g,''));
        return {
          date: c[idx.date],
          open: idx.open >= 0 ? parseFloat(c[idx.open]) : null,
          high: idx.high >= 0 ? parseFloat(c[idx.high]) : null,
          low: idx.low >= 0 ? parseFloat(c[idx.low]) : null,
          close: parseFloat(c[idx.close]),
          volume: idx.volume >= 0 ? parseFloat(c[idx.volume]) : 0,
        };
      }).filter(d => !isNaN(d.close));

      rawData.sort((a,b) => a.date.localeCompare(b.date));
      log(`CSVロード: ${rawData.length}件`, 'ok');
      document.getElementById('stat-symbol').textContent = `CSV (${rawData.length}件)`;
      initRange();
      applyRange();
    } catch(e) { log(`CSV解析エラー: ${e.message}`, 'err'); }
  };
  reader.readAsText(file);
}

// ─── Range ────────────────────────────────────────────────
function initRange() {
  if (!rawData.length) return;
  const dates = rawData.map(d => d.date);
  document.getElementById('rangeStart').value = dates[0];
  document.getElementById('rangeEnd').value = dates[dates.length-1];
  document.getElementById('nDays').max = rawData.length;
}
function applyRange() {
  if (!rawData.length) { log('データがありません', 'err'); return; }
  const nDays = parseInt(document.getElementById('nDays').value);
  const start = document.getElementById('rangeStart').value;
  const end = document.getElementById('rangeEnd').value;

  filteredData = rawData.filter(d => (!start || d.date >= start) && (!end || d.date <= end));
  if (filteredData.length > nDays) filteredData = filteredData.slice(-nDays);

  const dates = filteredData.map(d=>d.date);
  document.getElementById('stat-range').textContent = `範囲: ${dates[0]} ─ ${dates[dates.length-1]} (${filteredData.length}日)`;
  log(`範囲適用: ${filteredData.length}件`, 'ok');
  runAnalysis();
}

// ─── Main Analysis ─────────────────────────────────────────
function runAnalysis() {
  if (!filteredData.length) { log('フィルタ後データなし', 'err'); return; }
  const closes = filteredData.map(d => d.close);
  const returns = closes.slice(1).map((c,i) => (c - closes[i]) / closes[i] * 100);

  buildOverview(closes, returns);
  buildPriceChart(closes);
  buildVolatilityTab(closes, returns);
  buildDistribution(returns);
  buildProbTable();
}

// ─── OVERVIEW ─────────────────────────────────────────────
function buildOverview(closes, returns) {
  const first = closes[0], last = closes[closes.length-1];
  const totalRet = (last - first) / first * 100;
  const volAnnual = std(returns) * Math.sqrt(252);
  const maxDD = calcMaxDD(closes);
  const sharpe = (avg(returns) / std(returns)) * Math.sqrt(252);

  const kpiArea = document.getElementById('kpi-area');
  kpiArea.innerHTML = `
    <div class="kpi-card">
      <div class="kpi-label">現在価格</div>
      <div class="kpi-value neutral">${fmtN(last,0)}</div>
      <div class="kpi-sub">始値: ${fmtN(first,0)}</div>
    </div>
    <div class="kpi-card">
      <div class="kpi-label">期間リターン</div>
      <div class="kpi-value ${totalRet>=0?'up':'down'}">${fmtPct(totalRet)}</div>
      <div class="kpi-sub">日次平均: ${fmtPct(avg(returns))}</div>
    </div>
    <div class="kpi-card">
      <div class="kpi-label">年率ボラティリティ</div>
      <div class="kpi-value ${volAnnual>30?'down':volAnnual>15?'neutral':'up'}">${fmtN(volAnnual,1)}%</div>
      <div class="kpi-sub">日次σ: ${fmtN(std(returns),2)}%</div>
    </div>
    <div class="kpi-card">
      <div class="kpi-label">最大ドローダウン</div>
      <div class="kpi-value down">-${fmtN(maxDD,1)}%</div>
      <div class="kpi-sub">&nbsp;</div>
    </div>
    <div class="kpi-card">
      <div class="kpi-label">Sharpe比 (年率)</div>
      <div class="kpi-value ${sharpe>=1?'up':sharpe>=0?'neutral':'down'}">${fmtN(sharpe,2)}</div>
      <div class="kpi-sub">リスク調整リターン</div>
    </div>
    <div class="kpi-card">
      <div class="kpi-label">期間データ数</div>
      <div class="kpi-value neutral">${filteredData.length}</div>
      <div class="kpi-sub">${filteredData[0]?.date} ─</div>
    </div>
  `;

  // Trend summary
  const maS = parseInt(document.getElementById('maShort').value)||5;
  const maL = parseInt(document.getElementById('maLong').value)||25;
  const maShortArr = ma(closes, maS);
  const maLongArr = ma(closes, maL);
  const lastS = maShortArr[maShortArr.length-1];
  const lastL = maLongArr[maLongArr.length-1];
  const trendBull = lastS > lastL;
  const posRetPct = returns.filter(r=>r>0).length / returns.length * 100;

  document.getElementById('trend-summary').innerHTML = `
    <div class="section-title" style="margin-bottom:10px">トレンドサマリー</div>
    <div style="display:flex;gap:12px;flex-wrap:wrap;align-items:center">
      <span class="trend-badge ${trendBull?'trend-bull':'trend-bear'}">${trendBull?'▲ BULLISH':'▼ BEARISH'}</span>
      <span style="color:var(--text2);font-size:12px">MA${maS}(${fmtN(lastS,0)}) vs MA${maL}(${fmtN(lastL,0)})</span>
      <span style="color:var(--text2);font-size:12px">上昇日: ${fmtN(posRetPct,1)}%</span>
      <span class="trend-badge ${volAnnual>30?'trend-bear':volAnnual>15?'trend-neutral':'trend-bull'}">ボラ: ${volAnnual>30?'HIGH':volAnnual>15?'MED':'LOW'}</span>
    </div>
    <div style="margin-top:12px">
      <div class="prob-bar-wrap">
        <div class="prob-bar-label"><span>上昇日比率</span><span>${fmtN(posRetPct,1)}%</span></div>
        <div class="prob-bar-bg"><div class="prob-bar-fill" style="width:${posRetPct}%;background:var(--up)"></div></div>
      </div>
      <div class="prob-bar-wrap">
        <div class="prob-bar-label"><span>ボラティリティ水準</span><span>${fmtN(Math.min(volAnnual,80)/80*100,0)}% of scale</span></div>
        <div class="prob-bar-bg"><div class="prob-bar-fill" style="width:${Math.min(volAnnual,80)/80*100}%;background:${volAnnual>30?'var(--down)':volAnnual>15?'var(--warn)':'var(--up)'}"></div></div>
      </div>
    </div>
  `;

  // Overview price chart
  destroyChart('overview-price');
  const ctx1 = document.getElementById('chart-overview-price').getContext('2d');
  const labels = filteredData.map(d=>d.date);
  charts['overview-price'] = new Chart(ctx1, {
    type: 'line',
    data: { labels, datasets: [{ label: '終値', data: closes, borderColor: '#00e5ff', borderWidth: 1.5, pointRadius: 0, fill: { target: 'origin', above: 'rgba(0,229,255,0.05)' } }] },
    options: chartOpts('価格', true)
  });

  // Return histogram
  destroyChart('overview-dist');
  const bins = buildBins(returns, 0.5);
  const ctx2 = document.getElementById('chart-overview-dist').getContext('2d');
  charts['overview-dist'] = new Chart(ctx2, {
    type: 'bar',
    data: { labels: bins.labels, datasets: [{ label: '頻度', data: bins.counts, backgroundColor: bins.labels.map(l => parseFloat(l) >= 0 ? 'rgba(0,230,118,0.6)' : 'rgba(255,23,68,0.6)'), borderWidth: 0 }] },
    options: chartOpts('日次リターン%')
  });

  document.getElementById('dot-vol').className = `dot ${volAnnual>30?'err':volAnnual>15?'warn':'ok'}`;
  document.getElementById('stat-vol').textContent = `ボラ: ${fmtN(volAnnual,1)}% (年率)`;
}

// ─── PRICE CHART ───────────────────────────────────────────
function buildPriceChart(closes) {
  const maS = parseInt(document.getElementById('maShort').value)||5;
  const maL = parseInt(document.getElementById('maLong').value)||25;
  const labels = filteredData.map(d=>d.date);
  const volumes = filteredData.map(d=>d.volume||0);

  destroyChart('price');
  const ctx = document.getElementById('chart-price').getContext('2d');
  charts['price'] = new Chart(ctx, {
    type: 'line',
    data: {
      labels,
      datasets: [
        { label: '終値', data: closes, borderColor: '#00e5ff', borderWidth: 1.5, pointRadius: 0, order: 1 },
        { label: `MA${maS}`, data: ma(closes, maS), borderColor: '#ffea00', borderWidth: 1, pointRadius: 0, borderDash: [3,3], order: 2 },
        { label: `MA${maL}`, data: ma(closes, maL), borderColor: '#ff6b35', borderWidth: 1, pointRadius: 0, borderDash: [6,3], order: 3 },
      ]
    },
    options: chartOpts('価格')
  });

  destroyChart('volume');
  const ctx2 = document.getElementById('chart-volume').getContext('2d');
  charts['volume'] = new Chart(ctx2, {
    type: 'bar',
    data: { labels, datasets: [{ label: '出来高', data: volumes, backgroundColor: 'rgba(0,229,255,0.3)', borderWidth: 0 }] },
    options: chartOpts('出来高')
  });
}

// ─── VOLATILITY ────────────────────────────────────────────
function buildVolatilityTab(closes, returns) {
  const volW = parseInt(document.getElementById('volWindow').value)||20;
  const labels = filteredData.map(d=>d.date).slice(volW);

  const rollingVol = [];
  for (let i = volW; i <= returns.length; i++) {
    rollingVol.push(std(returns.slice(i-volW, i)) * Math.sqrt(252));
  }

  destroyChart('vol');
  const ctx = document.getElementById('chart-vol').getContext('2d');
  const avgVol = avg(rollingVol);
  charts['vol'] = new Chart(ctx, {
    type: 'line',
    data: {
      labels,
      datasets: [
        { label: `ローリングVol(${volW}日)`, data: rollingVol, borderColor: '#ff6b35', borderWidth: 1.5, pointRadius: 0, fill: { target: 'origin', above: 'rgba(255,107,53,0.08)' } },
        { label: '平均', data: new Array(labels.length).fill(avgVol), borderColor: '#ffea00', borderWidth: 1, borderDash: [4,4], pointRadius: 0 },
      ]
    },
    options: chartOpts('ボラ(年率%)')
  });

  // Classification
  const highThresh = avgVol * 1.5;
  const lowThresh = avgVol * 0.6;
  const highVol = rollingVol.filter(v => v > highThresh);
  const lowVol = rollingVol.filter(v => v < lowThresh);
  const medVol = rollingVol.filter(v => v >= lowThresh && v <= highThresh);

  document.getElementById('vol-classification').innerHTML = `
    <div class="vol-indicator high"><div><div style="font-family:'Space Mono',monospace;font-size:13px;color:var(--down)">高ボラ期</div><div style="font-size:10px;color:var(--text2)">>${fmtN(highThresh,1)}% 年率</div></div><div style="margin-left:auto;font-family:'Space Mono',monospace;font-size:16px;color:var(--down)">${highVol.length}日 (${fmtN(highVol.length/rollingVol.length*100,0)}%)</div></div>
    <div class="vol-indicator med"><div><div style="font-family:'Space Mono',monospace;font-size:13px;color:var(--warn)">中ボラ期</div><div style="font-size:10px;color:var(--text2)">${fmtN(lowThresh,1)}─${fmtN(highThresh,1)}%</div></div><div style="margin-left:auto;font-family:'Space Mono',monospace;font-size:16px;color:var(--warn)">${medVol.length}日 (${fmtN(medVol.length/rollingVol.length*100,0)}%)</div></div>
    <div class="vol-indicator low"><div><div style="font-family:'Space Mono',monospace;font-size:13px;color:var(--up)">低ボラ期</div><div style="font-size:10px;color:var(--text2)"><${fmtN(lowThresh,1)}%</div></div><div style="margin-left:auto;font-family:'Space Mono',monospace;font-size:16px;color:var(--up)">${lowVol.length}日 (${fmtN(lowVol.length/rollingVol.length*100,0)}%)</div></div>
    <div style="margin-top:8px;font-size:11px;color:var(--text2)">
      高ボラ期 平均日次リターン: <span style="color:var(--accent)">${fmtPct(getReturnsByVolRegime(returns, rollingVol, volW, 'high', highThresh))}</span><br>
      低ボラ期 平均日次リターン: <span style="color:var(--accent)">${fmtPct(getReturnsByVolRegime(returns, rollingVol, volW, 'low', lowThresh))}</span>
    </div>
  `;

  // Vol vs return bar
  const hR = getReturnsArrByVolRegime(returns, rollingVol, volW, 'high', highThresh);
  const lR = getReturnsArrByVolRegime(returns, rollingVol, volW, 'low', lowThresh);
  const mR = getReturnsArrByVolRegime(returns, rollingVol, volW, 'med', null, lowThresh, highThresh);
  const binsH = buildBins(hR, 0.5);
  const binsL = buildBins(lR, 0.5);
  const allLabels = [...new Set([...binsH.labels, ...binsL.labels])].sort((a,b)=>parseFloat(a)-parseFloat(b));

  destroyChart('vol-return');
  const ctx2 = document.getElementById('chart-vol-return').getContext('2d');
  charts['vol-return'] = new Chart(ctx2, {
    type: 'bar',
    data: {
      labels: allLabels,
      datasets: [
        { label: '高ボラ期', data: allLabels.map(l => (binsH.map[l]||0)), backgroundColor: 'rgba(255,23,68,0.5)', borderWidth: 0 },
        { label: '低ボラ期', data: allLabels.map(l => (binsL.map[l]||0)), backgroundColor: 'rgba(0,230,118,0.5)', borderWidth: 0 },
      ]
    },
    options: { ...chartOpts('頻度'), scales: { x: { stacked: false, ...axisStyle() }, y: { ...axisStyle() } } }
  });
}

function getReturnsByVolRegime(returns, rollingVol, volW, regime, thresh, low, high) {
  const arr = getReturnsArrByVolRegime(returns, rollingVol, volW, regime, thresh, low, high);
  return arr.length ? avg(arr) : NaN;
}
function getReturnsArrByVolRegime(returns, rollingVol, volW, regime, thresh, low, high) {
  const arr = [];
  for (let i = 0; i < rollingVol.length; i++) {
    const v = rollingVol[i];
    const ri = i + volW;
    if (ri >= returns.length) break;
    let match = false;
    if (regime === 'high') match = v > thresh;
    else if (regime === 'low') match = v < thresh;
    else match = v >= low && v <= high;
    if (match) arr.push(returns[ri]);
  }
  return arr;
}

// ─── DISTRIBUTION ──────────────────────────────────────────
function buildDistribution(returns) {
  const binSize = parseFloat(document.getElementById('binSize').value)||1;
  const bins = buildBins(returns, binSize);

  destroyChart('hist');
  const ctx1 = document.getElementById('chart-hist').getContext('2d');
  charts['hist'] = new Chart(ctx1, {
    type: 'bar',
    data: {
      labels: bins.labels,
      datasets: [{
        label: '頻度',
        data: bins.counts,
        backgroundColor: bins.labels.map(l => parseFloat(l) >= 0 ? 'rgba(0,230,118,0.6)' : 'rgba(255,23,68,0.6)'),
        borderWidth: 0
      }]
    },
    options: chartOpts('日次リターン%')
  });

  // ACF
  const lags = 20;
  const acfVals = [];
  const m = avg(returns);
  const denom = returns.reduce((a,r)=>a+(r-m)**2, 0);
  for (let k=0; k<=lags; k++) {
    let num = 0;
    for (let i=k; i<returns.length; i++) num += (returns[i]-m)*(returns[i-k]-m);
    acfVals.push(num/denom);
  }
  destroyChart('acf');
  const ctx2 = document.getElementById('chart-acf').getContext('2d');
  const confInterval = 1.96 / Math.sqrt(returns.length);
  charts['acf'] = new Chart(ctx2, {
    type: 'bar',
    data: {
      labels: acfVals.map((_,i) => `L${i}`),
      datasets: [
        { label: 'ACF', data: acfVals, backgroundColor: acfVals.map((v,i) => i===0?'transparent':Math.abs(v)>confInterval?'rgba(255,234,0,0.7)':'rgba(0,229,255,0.4)'), borderWidth: 0 },
        { label: '+95%CI', data: new Array(lags+1).fill(confInterval), type: 'line', borderColor: 'rgba(255,107,53,0.6)', borderDash:[4,4], borderWidth:1, pointRadius:0 },
        { label: '-95%CI', data: new Array(lags+1).fill(-confInterval), type: 'line', borderColor: 'rgba(255,107,53,0.6)', borderDash:[4,4], borderWidth:1, pointRadius:0 },
      ]
    },
    options: chartOpts('自己相関係数')
  });

  // Monthly returns
  const monthly = {};
  filteredData.forEach((d, i) => {
    if (i === 0) return;
    const m = d.date.slice(0,7);
    if (!monthly[m]) monthly[m] = [];
    const r = (filteredData[i].close - filteredData[i-1].close) / filteredData[i-1].close * 100;
    monthly[m].push(r);
  });
  const mLabels = Object.keys(monthly).sort();
  const mVals = mLabels.map(k => avg(monthly[k]));
  destroyChart('monthly');
  const ctx3 = document.getElementById('chart-monthly').getContext('2d');
  charts['monthly'] = new Chart(ctx3, {
    type: 'bar',
    data: {
      labels: mLabels,
      datasets: [{ label: '月平均リターン%', data: mVals, backgroundColor: mVals.map(v => v>=0?'rgba(0,230,118,0.6)':'rgba(255,23,68,0.6)'), borderWidth: 0 }]
    },
    options: chartOpts('%')
  });
}

// ─── PROBABILITY ───────────────────────────────────────────
function buildProbTable() {
  if (!filteredData.length) return;
  const closes = filteredData.map(d => d.close);
  const last = closes[closes.length - 1];
  const refInput = document.getElementById('refPrice').value;
  const ref = refInput ? parseFloat(refInput) : last;
  const tol = parseFloat(document.getElementById('tolerance').value) || 2;

  const returns = closes.slice(1).map((c,i)=>(c-closes[i])/closes[i]*100);

  const ranges = [-10, -5, -3, -2, -1, -0.5, 0, 0.5, 1, 2, 3, 5, 10];
  const tableRows = [];
  for (let i=0; i<ranges.length-1; i++) {
    const lo = ranges[i], hi = ranges[i+1];
    const cnt = returns.filter(r => r >= lo && r < hi).length;
    const prob = cnt / returns.length * 100;
    tableRows.push({ range: `${fmtPct(lo)} ~ ${fmtPct(hi)}`, cnt, prob });
  }

  // Near-price probability
  const nearCount = closes.filter(c => Math.abs(c - ref) / ref * 100 <= tol).length;
  const nearProb = nearCount / closes.length * 100;

  document.getElementById('prob-table-area').innerHTML = `
    <div style="margin-bottom:12px;padding:10px;background:var(--bg);border-radius:4px;border:1px solid var(--border)">
      <span style="font-size:11px;color:var(--text2)">基準価格 </span><span style="font-family:'Space Mono',monospace;color:var(--accent)">${fmtN(ref,0)}</span>
      <span style="font-size:11px;color:var(--text2);margin-left:16px">±${tol}% 範囲内の出現確率: </span>
      <span style="font-family:'Space Mono',monospace;font-size:16px;color:${nearProb>30?'var(--up)':'var(--accent)'}">${fmtN(nearProb,1)}%</span>
      <span style="font-size:10px;color:var(--text2);margin-left:8px">(${nearCount}/${closes.length}日)</span>
    </div>
    <table class="dist-table">
      <thead><tr><th>翌日リターン帯</th><th>日数</th><th>確率</th><th>バー</th></tr></thead>
      <tbody>
        ${tableRows.map(r => `
          <tr>
            <td>${r.range}</td>
            <td>${r.cnt}</td>
            <td style="color:${r.prob>15?'var(--accent)':'var(--text)'}">${fmtN(r.prob,1)}%</td>
            <td style="width:120px"><div style="height:6px;background:var(--bg);border-radius:2px;overflow:hidden"><div style="width:${Math.min(r.prob*3,100)}%;height:100%;background:var(--accent);border-radius:2px"></div></div></td>
          </tr>
        `).join('')}
      </tbody>
    </table>
  `;

  // KDE
  buildKDE(returns);
}

function buildKDE(returns) {
  const min = Math.min(...returns) - 1;
  const max = Math.max(...returns) + 1;
  const step = 0.2;
  const xVals = [];
  for (let x = min; x <= max; x += step) xVals.push(x);

  const h = 1.06 * std(returns) * Math.pow(returns.length, -0.2);
  const kdeVals = xVals.map(x => {
    return returns.reduce((sum, r) => {
      const u = (x - r) / h;
      return sum + Math.exp(-0.5 * u * u) / (Math.sqrt(2 * Math.PI));
    }, 0) / (returns.length * h);
  });

  // Normal distribution for comparison
  const m = avg(returns), s = std(returns);
  const normalVals = xVals.map(x => Math.exp(-0.5*((x-m)/s)**2) / (s * Math.sqrt(2*Math.PI)));

  destroyChart('kde');
  const ctx = document.getElementById('chart-kde').getContext('2d');
  charts['kde'] = new Chart(ctx, {
    type: 'line',
    data: {
      labels: xVals.map(v => fmtN(v,1)),
      datasets: [
        { label: '実績KDE', data: kdeVals, borderColor: '#00e5ff', borderWidth: 2, pointRadius: 0, fill: { target: 'origin', above: 'rgba(0,229,255,0.08)' } },
        { label: '正規分布', data: normalVals, borderColor: '#ff6b35', borderWidth: 1.5, borderDash: [6,3], pointRadius: 0 },
      ]
    },
    options: chartOpts('確率密度')
  });
}

// ─── Helpers ───────────────────────────────────────────────
function calcMaxDD(closes) {
  let peak = closes[0], maxDD = 0;
  for (const c of closes) {
    if (c > peak) peak = c;
    const dd = (peak - c) / peak * 100;
    if (dd > maxDD) maxDD = dd;
  }
  return maxDD;
}
function buildBins(returns, step) {
  const min = Math.floor(Math.min(...returns)/step)*step;
  const max = Math.ceil(Math.max(...returns)/step)*step;
  const labels = [], counts = [], map = {};
  for (let v = min; v <= max; v += step) {
    const l = fmtN(v,1);
    labels.push(l);
    const cnt = returns.filter(r => r >= v && r < v+step).length;
    counts.push(cnt);
    map[l] = cnt;
  }
  return { labels, counts, map };
}

function axisStyle() {
  return {
    ticks: { color: '#6a8090', font: { family: "'Space Mono'" , size: 9 } },
    grid: { color: 'rgba(36,44,58,0.8)' },
    border: { color: '#242c3a' }
  };
}
function chartOpts(yLabel, tension=false) {
  return {
    responsive: true,
    maintainAspectRatio: true,
    interaction: { mode: 'index', intersect: false },
    plugins: {
      legend: { labels: { color: '#6a8090', font: { family: "'Space Mono'", size: 10 }, boxWidth: 20 } },
      tooltip: {
        backgroundColor: 'rgba(17,20,24,0.95)',
        borderColor: '#242c3a', borderWidth: 1,
        titleColor: '#00e5ff', bodyColor: '#c8d8e8',
        titleFont: { family: "'Space Mono'" }, bodyFont: { family: "'Space Mono'", size: 11 }
      }
    },
    elements: { line: { tension: tension ? 0.2 : 0 } },
    scales: { x: axisStyle(), y: { ...axisStyle(), title: { display: !!yLabel, text: yLabel, color: '#6a8090', font: { size: 10 } } } }
  };
}

// ─── Tab switch ────────────────────────────────────────────
function switchTab(name) {
  document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
  document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
  event.target.classList.add('active');
  document.getElementById('tab-' + name).classList.add('active');
}

// ─── Demo / No data state ──────────────────────────────────
document.querySelectorAll('.tab-content').forEach(tc => {
  if (!tc.querySelector('canvas')) return;
  // leave empty until data loads
});
log('データを取得またはCSVをロードしてください', 'info');
</script>
</body>
</html>

いいなと思ったら応援しよう!

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
株価分析ツール「Stock Chart」|古井和雄
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word

mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1