株価分析ツール「Stock Chart」
・ダウンロードされる方はこちら。↓
・ソースコードはこちら。↓
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Stock Chart 1.0</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>銘柄シンボル (Yahoo Finance)</label>
<input type="text" id="symbolInput" placeholder="例: 7203.T, AAPL, BTC-USD" value="7203.T">
<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から取得</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>
</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 via CORS proxy ──────────────────────────────
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} / ${period} / ${interval}`, 'info');
// Yahoo Finance v2 via allorigins proxy
const yahooUrl = `https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(sym)}?range=${period}&interval=${interval}&includePrePost=false`;
const proxyUrl = `https://api.allorigins.win/get?url=${encodeURIComponent(yahooUrl)}`;
try {
const res = await fetch(proxyUrl);
const json = await res.json();
const data = JSON.parse(json.contents);
const result = data.chart.result?.[0];
if (!result) throw new Error('データなし。シンボルを確認してください');
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(`取得完了: ${rawData.length}件`, 'ok');
document.getElementById('stat-symbol').textContent = `${sym} (${rawData.length}件)`;
initRange();
applyRange();
} catch(e) {
log(`エラー: ${e.message}`, 'err');
log('CORSエラーの場合はCSVをお使いください', 'info');
} finally { showLoading(false); }
}
// ─── 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"> </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>

コメント