平均分散アプローチを計算できるアプリの作り方

投資・ファイナンス

今回は、以下のページのような平均分散アプローチを計算できる簡単なアプリの作り方を紹介したいと思います。

構成

このアプリでは、ユーザーが資産の情報として、
リターン、リスク、資産間の相関係数を入力します。

この入力内容に基づいて資産の組み合わせで生成される効率的フロンティアを描画します。

最小分散ポートフォリオと、一般的な効用関数の最大化の結果も表示します。(簡易的な計算)

実装にはJavascriptで簡易的な最適化計算が可能なnumericを使いました。

より高度な最適化を行うにはやはりPythonが便利です。

コード全体

全体のコードは以下です。


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Efficient Frontier with Plotly.js</title>
    <link rel="stylesheet" href="style.css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/numeric/1.2.6/numeric.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/mathjs"></script>
    <script src="https://cdn.plot.ly/plotly-2.24.2.min.js"></script>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
            line-height: 1.6;
        }

        h1, h2 {
            color: #333;
        }


        .form-group {
            display: grid;
            grid-template-columns: 1fr 2fr;
            gap: 10px;
            align-items: center;
            margin-bottom: 15px;
        }

        .form-group label {
            font-weight: bold;
        }

        input[type="text"], input[type="number"] {
            padding: 8px;
            border: 1px solid #ccc;
            border-radius: 4px;
        }

        .btn {
            background: #007bff;
            color: #fff;
            padding: 10px 20px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }

        .btn:hover {
            background: #0056b3;
        }

        .matrix-input {
            margin-top: 15px;
            padding: 10px;
            background: #fff;
            border: 1px solid #ddd;
            border-radius: 4px;
        }

        #results {
            margin-top: 20px;
        }

        #plot {
            margin-top: 40px;
        }
    </style>
</head>
<body>

        <h1>Efficient Frontier with Plotly.js</h1>

        <!-- 資産情報入力フォーム -->
        <div>
            <h2>資産情報を追加</h2>
            <form id="assetForm">
                <div class="form-group">
                    <label for="assetName">資産名:</label>
                    <input type="text" id="assetName" required>
                </div>
                <div class="form-group">
                    <label for="assetReturn">リターン (%):</label>
                    <input type="number" id="assetReturn" required>
                </div>
                <div class="form-group">
                    <label for="assetRisk">リスク (%):</label>
                    <input type="number" id="assetRisk" required>
                </div>
                <button type="button" class="btn" id="addAsset">資産を追加</button>
            </form>
        </div>

        <!-- 相関係数入力 -->
        <div id="correlationMatrixSection" style="display: none;">
            <h2>相関係数を入力</h2>
            <p>資産間の相関係数を入力してください (−1から1までの値)。</p>
            <div id="correlationMatrix" class="matrix-input"></div>
        </div>

        <!-- 効用関数とリスク回避度設定 -->
        <div>
            <h2>リスク回避度</h2>
            <div class="form-group">
                <label for="riskAversion">リスク回避度 λ (例: 2):</label>
                <input type="number" id="riskAversion" step="0.1" value="2">
            </div>
        </div>

        <!-- 計算結果 -->
        <div>
            <h2>計算結果</h2>
            <button class="btn" id="calculatePortfolio">ポートフォリオを計算</button>
            <div id="results"></div>
        </div>

        <div id="plot" style="width:800px; height:600px;"></div>

    <script>

        const assets = [];
        let correlationMatrix = [];


        // 資産を追加
        document.getElementById('addAsset').addEventListener('click', () => {
            const name = document.getElementById('assetName').value;
            const ret = parseFloat(document.getElementById('assetReturn').value);
            const risk = parseFloat(document.getElementById('assetRisk').value);

            if (name && !isNaN(ret) && !isNaN(risk)) {
                assets.push({ name, ret: ret / 100, risk: risk / 100 });
                updateAssetList();
                updateCorrelationMatrix();
            }
        });

        // 資産リストの表示
        function updateAssetList() {
            const tbody = document.getElementById('results');
            tbody.innerHTML = assets.map(asset => `
                <div>
                    <strong>${asset.name}</strong>: リターン ${(asset.ret * 100).toFixed(2)}%、リスク ${(asset.risk * 100).toFixed(2)}%
                </div>
            `).join('');
        }


        // 相関係数入力欄の更新
        function updateCorrelationMatrix() {
            const section = document.getElementById('correlationMatrixSection');
            const matrixDiv = document.getElementById('correlationMatrix');

            if (assets.length > 1) {
                section.style.display = 'block';
                matrixDiv.innerHTML = '';

                correlationMatrix = Array(assets.length).fill(null).map(() =>
                    Array(assets.length).fill(1)
                );

                // 入力フィールドを作成 (i < j のときだけ作成)
                for (let i = 0; i < assets.length; i++) {
                    for (let j = i + 1; j < assets.length; j++) {
                        const label = document.createElement('label');
                        label.textContent = `${assets[i].name} - ${assets[j].name}: `;

                        const input = document.createElement('input');
                        input.type = 'number';
                        input.step = '0.01';
                        input.value = '0';
                        input.min = '-1';
                        input.max = '1';
                        input.dataset.row = i;
                        input.dataset.col = j;

                        input.addEventListener('change', (e) => {
                            const row = parseInt(e.target.dataset.row, 10);
                            const col = parseInt(e.target.dataset.col, 10);
                            const value = parseFloat(e.target.value);

                            if (value >= -1 && value <= 1) {
                                correlationMatrix[row][col] = value;
                                correlationMatrix[col][row] = value;
                            } else {
                                alert('相関係数は -1 から 1 の範囲で入力してください。');
                                e.target.value = '0'; // リセット
                            }
                        });

                        label.appendChild(input);
                        matrixDiv.appendChild(label);
                        matrixDiv.appendChild(document.createElement('br'));
                    }
                }
            } else {
                section.style.display = 'none';
            }
        }

        // 共分散行列を計算
        function calculateCovarianceMatrix() {
            return assets.map((a1, i) =>
                assets.map((a2, j) => correlationMatrix[i][j] * a1.risk * a2.risk)
            );
        }



        let expectedReturns = assets.map(a => a.ret);
        let covarianceMatrix = calculateCovarianceMatrix();


        // リスクを計算する関数
        function calculateRisk(weights) {
            return Math.sqrt(numeric.dot(weights, numeric.dot(covarianceMatrix, weights)));
        }

        // 最適化する関数
        function optimizePortfolio(targetReturn) {
            const n = expectedReturns.length;
            const initialWeights = Array(n).fill(1 / n); // 均等配分で初期化

            // 制約条件: 重みの合計が1、期待収益率がtargetReturn
            const constraints = (weights) => {
                const sumWeights = numeric.sum(weights);
                const portfolioReturn = numeric.dot(weights, expectedReturns);
                return [
                    sumWeights - 1, // 重みの合計が1
                    portfolioReturn - targetReturn // 期待収益率がtargetReturn
                ];
            };

            // 目的関数: ポートフォリオリスク(最小化)
            const objective = (weights) => {
                const penalty = constraints(weights).reduce((sum, c) => sum + Math.pow(c, 2), 0);
                return calculateRisk(weights) + penalty * 1e6; // 制約違反のペナルティを加える
            };

            // 最適化
            const result = numeric.uncmin(objective, initialWeights);
            return result.solution;
        }

        // 最小分散ポートフォリオの計算
        function calculateMinimumVariancePortfolio() {
            const n = assets.length;
            const allReturns = assets.map(a => a.ret);
            const allRisks = assets.map(a => a.risk);
            const covarianceMatrix = calculateCovarianceMatrix();

            // 最小分散ポートフォリオのウェイトを求める
            const ones = Array(n).fill(1);
            const covarianceInverse = math.inv(covarianceMatrix);

            const weights = math.multiply(covarianceInverse, ones);
            const sumWeights = math.sum(weights);
            return weights.map(w => w / sumWeights); // 重みを合計1になるように正規化
        }

        // 効率的フロンティアをプロット

        function plotEfficientFrontier() {
            expectedReturns = assets.map(a => a.ret);
            covarianceMatrix = calculateCovarianceMatrix();
            const riskAversionInput = document.getElementById('riskAversion');
            const riskAversion = parseFloat(riskAversionInput.value); // 入力されたリスク回避度を取得


            // expectedReturns の最小値と最大値を取得
            const minReturn = Math.min(...expectedReturns);
            const maxReturn = Math.max(...expectedReturns);

            // minReturn から maxReturn までの線形間隔を生成
            const targetReturns = numeric.linspace(minReturn, maxReturn * 1.5, 100);
            const risks = [];
            const weightsList = [];

            for (let i = 0; i < targetReturns.length; i++) {
                const weights = optimizePortfolio(targetReturns[i]);
                const risk = calculateRisk(weights);
                risks.push(risk);
                weightsList.push(weights); // 各リターンの重みを保存
            }

            // 効用を計算し、最大効用を求める
            const utilities = targetReturns.map((ret, i) => ret - (riskAversion / 2) * Math.pow(risks[i], 2));
            const maxUtilityIndex = utilities.indexOf(Math.max(...utilities));
            const maxUtilityRisk = risks[maxUtilityIndex];
            const maxUtilityReturn = targetReturns[maxUtilityIndex];
            const maxUtilityWeights = weightsList[maxUtilityIndex];

            // assets の ret と risk を取り出してプロット
            const retValues = assets.map(a => a.ret);
            const riskValues = assets.map(a => a.risk);
            const assetNames = assets.map(a => a.name);


            // 最小リスクを求める
            const minRiskIndex = risks.indexOf(Math.min(...risks));
            const minRisk = risks[minRiskIndex];
            const minRiskWeights = weightsList[minRiskIndex]; // 最小リスクに対応するウェイト

            // 結果をHTMLに表示
            const resultsDiv = document.getElementById('results');
            resultsDiv.innerHTML = ''; // 既存の内容をクリア

            resultsDiv.innerHTML += `
                <h3>最小分散ポートフォリオ</h3>
                ${minRiskWeights.map((w, i) => `<div>${assets[i].name}: ${(w * 100).toFixed(2)}%</div>`).join('')}

                <h3>効用最大化ポートフォリオ</h3>
                ${maxUtilityWeights.map((w, i) => `<div>${assets[i].name}: ${(w * 100).toFixed(2)}%</div>`).join('')}
                <div>期待リターン: ${(maxUtilityReturn * 100).toFixed(2)}%</div>
                <div>リスク (標準偏差): ${(maxUtilityRisk * 100).toFixed(2)}%</div>
            `;

            // 効率的フロンティアのホバーテキストを作成
            const efficientFrontierHoverText = weightsList.map((weights, i) => 
                `Expected Return: ${(targetReturns[i] * 100).toFixed(2)}%<br>` +
                `Risk: ${(risks[i] * 100).toFixed(2)}%<br>` +
                `Weights:<br>` +
                weights.map((w, j) => `${assets[j].name}: ${(w * 100).toFixed(2)}%`).join('<br>')
            );

            // Plotlyで描画
            const trace = {
                x: risks, // リスク(標準偏差)
                y: targetReturns, // 期待収益率
                mode: 'lines+markers',
                type: 'scatter',
                name: 'Efficient Frontier',
                line: { color: 'blue' },
                marker: { size: 6 },
                text: efficientFrontierHoverText, // ホバー時のテキスト
                hoverinfo: 'text' // ホバー時にテキストのみ表示
            };

            const trace2 = {
                x: riskValues,
                y: retValues,
                mode: 'markers+text',
                type: 'scatter',
                name: 'Assets',
                marker: { color: 'red', size: 8 },
                text: assetNames,
                textposition: 'top center',
                hoverinfo: 'text' // 資産名をホバーで表示
            };

            // 最小分散ポートフォリオのホバーテキスト
            const minRiskHoverText = 
                `Minimum Variance Portfolio<br>` +
                `Expected Return: ${(targetReturns[minRiskIndex] * 100).toFixed(2)}%<br>` +
                `Risk: ${(minRisk * 100).toFixed(2)}%<br>` +
                `Weights:<br>` +
                minRiskWeights.map((w, i) => `${assets[i].name}: ${(w * 100).toFixed(2)}%`).join('<br>');

            const trace3 = {
                x: [minRisk],
                y: [targetReturns[minRiskIndex]],
                mode: 'markers',
                type: 'scatter',
                name: 'Minimum Variance Portfolio',
                marker: { color: 'green', size: 12 },
                text: [minRiskHoverText], // ホバー時のテキスト
                hoverinfo: 'text'
            };

            // 効用最大化ポートフォリオのホバーテキスト
            const maxUtilityHoverText = 
                `Maximum Utility Portfolio<br>` +
                `Expected Return: ${(maxUtilityReturn * 100).toFixed(2)}%<br>` +
                `Risk: ${(maxUtilityRisk * 100).toFixed(2)}%<br>` +
                `Weights:<br>` +
                maxUtilityWeights.map((w, i) => `${assets[i].name}: ${(w * 100).toFixed(2)}%`).join('<br>');

            const trace4 = {
                x: [maxUtilityRisk],
                y: [maxUtilityReturn],
                mode: 'markers',
                type: 'scatter',
                name: 'Maximum Utility Portfolio',
                marker: { color: 'orange', size: 12 },
                text: [maxUtilityHoverText], // ホバー時のテキスト
                hoverinfo: 'text'
            };

            const layout = {
                title: 'Efficient Frontier',
                xaxis: { title: 'Risk (Standard Deviation)' },
                yaxis: { title: 'Expected Return' },
                width: 800,
                height: 600
            };


            Plotly.newPlot('plot', [trace, trace2, trace3, trace4], layout);
        }



        // ポートフォリオ計算
        document.getElementById('calculatePortfolio').addEventListener('click', () => {
            if (assets.length < 2) {
                alert('少なくとも2つの資産を追加してください');
                return;
            }

            covarianceMatrix = calculateCovarianceMatrix();



            // 効率的フロンティアを描画
            plotEfficientFrontier();

        });




    </script>
</body>
</html>


コード解説

このコードは、資産のリスクとリターンを考慮してポートフォリオの効率的フロンティア(Efficient Frontier)を計算し、可視化するためのものです。以下に、コードをいくつかのパートに分けて解説します。


HTML ヘッダーとスタイル

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Efficient Frontier with Plotly.js</title>
<link rel="stylesheet" href="style.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/numeric/1.2.6/numeric.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/mathjs"></script>
<script src="https://cdn.plot.ly/plotly-2.24.2.min.js"></script>
<style>
    /* 各種スタイル定義 */
</style>
</head>
  • 機能:
  • 必要な外部ライブラリを読み込む:
    • Numeric.js: 数値計算のためのライブラリ。
    • Math.js: 行列計算や数学的操作のためのライブラリ。
    • Plotly.js: グラフ描画のためのライブラリ。
  • スタイルの適用:
    • フォームの見た目やボタンのスタイル。
    • グラフ表示用のコンテナスタイル。

資産情報入力フォーム

<form id="assetForm">
    <div class="form-group">
        <label for="assetName">資産名:</label>
        <input type="text" id="assetName" required>
    </div>
    <div class="form-group">
        <label for="assetReturn">リターン (%):</label>
        <input type="number" id="assetReturn" required>
    </div>
    <div class="form-group">
        <label for="assetRisk">リスク (%):</label>
        <input type="number" id="assetRisk" required>
    </div>
    <button type="button" class="btn" id="addAsset">資産を追加</button>
</form>
  • 機能:
  • ユーザーが資産名、リターン、リスクを入力し、追加できるフォーム。
  • ボタンを押すと、入力内容がリストに追加される。

  • 特徴:

  • JavaScriptのイベントリスナーを使用して、資産情報を収集。

相関係数の入力フォーム

<div id="correlationMatrixSection" style="display: none;">
    <h2>相関係数を入力</h2>
    <div id="correlationMatrix" class="matrix-input"></div>
</div>
  • 機能:
  • 資産間の相関係数を入力するためのセクション。
  • 資産が2つ以上になると、このセクションが表示される。

  • 特徴:

  • 相関係数は から の範囲で入力可能。
  • JavaScriptで動的に入力フィールドを生成。

資産リストの更新と相関係数入力の動的生成

const assets = [];
let correlationMatrix = [];

document.getElementById('addAsset').addEventListener('click', () => {
    const name = document.getElementById('assetName').value;
    const ret = parseFloat(document.getElementById('assetReturn').value);
    const risk = parseFloat(document.getElementById('assetRisk').value);

    if (name && !isNaN(ret) && !isNaN(risk)) {
        assets.push({ name, ret: ret / 100, risk: risk / 100 });
        updateAssetList();
        updateCorrelationMatrix();
    }
});

function updateAssetList() {
    const tbody = document.getElementById('results');
    tbody.innerHTML = assets.map(asset => `
        <div>
            <strong>${asset.name}</strong>: リターン ${(asset.ret * 100).toFixed(2)}%、リスク ${(asset.risk * 100).toFixed(2)}%
        </div>
    `).join('');
}

function updateCorrelationMatrix() {
    const section = document.getElementById('correlationMatrixSection');
    const matrixDiv = document.getElementById('correlationMatrix');

    if (assets.length > 1) {
        section.style.display = 'block';
        matrixDiv.innerHTML = '';

        correlationMatrix = Array(assets.length).fill(null).map(() =>
            Array(assets.length).fill(1)
        );

        for (let i = 0; i < assets.length; i++) {
            for (let j = i + 1; j < assets.length; j++) {
                const label = document.createElement('label');
                label.textContent = `${assets[i].name} - ${assets[j].name}: `;

                const input = document.createElement('input');
                input.type = 'number';
                input.step = '0.01';
                input.value = '0';
                input.min = '-1';
                input.max = '1';
                input.dataset.row = i;
                input.dataset.col = j;

                input.addEventListener('change', (e) => {
                    const row = parseInt(e.target.dataset.row, 10);
                    const col = parseInt(e.target.dataset.col, 10);
                    const value = parseFloat(e.target.value);

                    if (value >= -1 && value <= 1) {
                        correlationMatrix[row][col] = value;
                        correlationMatrix[col][row] = value;
                    } else {
                        alert('相関係数は -1 から 1 の範囲で入力してください。');
                        e.target.value = '0'; 
                    }
                });

                label.appendChild(input);
                matrixDiv.appendChild(label);
                matrixDiv.appendChild(document.createElement('br'));
            }
        }
    } else {
        section.style.display = 'none';
    }
}
  • 機能:
  • 資産情報を配列に追加し、画面に表示。
  • 相関係数の入力欄を動的に生成し、変更を反映。

  • ポイント:

  • 相関行列は対称行列であるため、 の両方を更新。

効用最大値の特定とプロット

効用の最大値とそれに対応するポートフォリオを特定し、それをプロットする部分を以下に解説します。

// 共分散行列を計算
function calculateCovarianceMatrix() {
    return assets.map((a1, i) =>
        assets.map((a2, j) => correlationMatrix[i][j] * a1.risk * a2.risk)
    );
}



let expectedReturns = assets.map(a => a.ret);
let covarianceMatrix = calculateCovarianceMatrix();


// リスクを計算する関数
function calculateRisk(weights) {
    return Math.sqrt(numeric.dot(weights, numeric.dot(covarianceMatrix, weights)));
}

// 最適化する関数
function optimizePortfolio(targetReturn) {
    const n = expectedReturns.length;
    const initialWeights = Array(n).fill(1 / n); // 均等配分で初期化

    // 制約条件: 重みの合計が1、期待収益率がtargetReturn
    const constraints = (weights) => {
        const sumWeights = numeric.sum(weights);
        const portfolioReturn = numeric.dot(weights, expectedReturns);
        return [
            sumWeights - 1, // 重みの合計が1
            portfolioReturn - targetReturn // 期待収益率がtargetReturn
        ];
    };

    // 目的関数: ポートフォリオリスク(最小化)
    const objective = (weights) => {
        const penalty = constraints(weights).reduce((sum, c) => sum + Math.pow(c, 2), 0);
        return calculateRisk(weights) + penalty * 1e6; // 制約違反のペナルティを加える
    };

    // 最適化
    const result = numeric.uncmin(objective, initialWeights);
    return result.solution;
}

// 最小分散ポートフォリオの計算
function calculateMinimumVariancePortfolio() {
    const n = assets.length;
    const allReturns = assets.map(a => a.ret);
    const allRisks = assets.map(a => a.risk);
    const covarianceMatrix = calculateCovarianceMatrix();

    // 最小分散ポートフォリオのウェイトを求める
    const ones = Array(n).fill(1);
    const covarianceInverse = math.inv(covarianceMatrix);

    const weights = math.multiply(covarianceInverse, ones);
    const sumWeights = math.sum(weights);
    return weights.map(w => w / sumWeights); // 重みを合計1になるように正規化
}


  1. 効用の計算
    各ターゲットリターン(targetReturns)に対して、効用(utility)を計算します。効用は以下の式に基づいて計算されます:


    riskAversion(リスク回避度)はユーザーが入力する値です。
    – 効用が最も高いインデックス(maxUtilityIndex)を取得します。

  2. 最大効用のリスク・リターンを特定
    最大効用のインデックスを基に、対応するリスク(maxUtilityRisk)とリターン(maxUtilityReturn)を特定します。これが、最も効率的なポートフォリオです。

  3. 最大効用に対応する重み
    最大効用に対応するポートフォリオの重み(maxUtilityWeights)を特定します。


効率的フロンティアのプロット

// 効率的フロンティアをプロット

function plotEfficientFrontier() {
    expectedReturns = assets.map(a => a.ret);
    covarianceMatrix = calculateCovarianceMatrix();
    const riskAversionInput = document.getElementById('riskAversion');
    const riskAversion = parseFloat(riskAversionInput.value); // 入力されたリスク回避度を取得


    // expectedReturns の最小値と最大値を取得
    const minReturn = Math.min(...expectedReturns);
    const maxReturn = Math.max(...expectedReturns);

    // minReturn から maxReturn までの線形間隔を生成
    const targetReturns = numeric.linspace(minReturn, maxReturn * 1.5, 100);
    const risks = [];
    const weightsList = [];

    for (let i = 0; i < targetReturns.length; i++) {
        const weights = optimizePortfolio(targetReturns[i]);
        const risk = calculateRisk(weights);
        risks.push(risk);
        weightsList.push(weights); // 各リターンの重みを保存
    }

    // 効用を計算し、最大効用を求める
    const utilities = targetReturns.map((ret, i) => ret - (riskAversion / 2) * Math.pow(risks[i], 2));
    const maxUtilityIndex = utilities.indexOf(Math.max(...utilities));
    const maxUtilityRisk = risks[maxUtilityIndex];
    const maxUtilityReturn = targetReturns[maxUtilityIndex];
    const maxUtilityWeights = weightsList[maxUtilityIndex];

    // assets の ret と risk を取り出してプロット
    const retValues = assets.map(a => a.ret);
    const riskValues = assets.map(a => a.risk);
    const assetNames = assets.map(a => a.name);


    // 最小リスクを求める
    const minRiskIndex = risks.indexOf(Math.min(...risks));
    const minRisk = risks[minRiskIndex];
    const minRiskWeights = weightsList[minRiskIndex]; // 最小リスクに対応するウェイト

    // 結果をHTMLに表示
    const resultsDiv = document.getElementById('results');
    resultsDiv.innerHTML = ''; // 既存の内容をクリア

    resultsDiv.innerHTML += `
        <h3>最小分散ポートフォリオ</h3>
        ${minRiskWeights.map((w, i) => `<div>${assets[i].name}: ${(w * 100).toFixed(2)}%</div>`).join('')}

        <h3>効用最大化ポートフォリオ</h3>
        ${maxUtilityWeights.map((w, i) => `<div>${assets[i].name}: ${(w * 100).toFixed(2)}%</div>`).join('')}
        <div>期待リターン: ${(maxUtilityReturn * 100).toFixed(2)}%</div>
        <div>リスク (標準偏差): ${(maxUtilityRisk * 100).toFixed(2)}%</div>
    `;

    // 効率的フロンティアのホバーテキストを作成
    const efficientFrontierHoverText = weightsList.map((weights, i) => 
        `Expected Return: ${(targetReturns[i] * 100).toFixed(2)}%<br>` +
        `Risk: ${(risks[i] * 100).toFixed(2)}%<br>` +
        `Weights:<br>` +
        weights.map((w, j) => `${assets[j].name}: ${(w * 100).toFixed(2)}%`).join('<br>')
    );

    // Plotlyで描画
    const trace = {
        x: risks, // リスク(標準偏差)
        y: targetReturns, // 期待収益率
        mode: 'lines+markers',
        type: 'scatter',
        name: 'Efficient Frontier',
        line: { color: 'blue' },
        marker: { size: 6 },
        text: efficientFrontierHoverText, // ホバー時のテキスト
        hoverinfo: 'text' // ホバー時にテキストのみ表示
    };

    const trace2 = {
        x: riskValues,
        y: retValues,
        mode: 'markers+text',
        type: 'scatter',
        name: 'Assets',
        marker: { color: 'red', size: 8 },
        text: assetNames,
        textposition: 'top center',
        hoverinfo: 'text' // 資産名をホバーで表示
    };

    // 最小分散ポートフォリオのホバーテキスト
    const minRiskHoverText = 
        `Minimum Variance Portfolio<br>` +
        `Expected Return: ${(targetReturns[minRiskIndex] * 100).toFixed(2)}%<br>` +
        `Risk: ${(minRisk * 100).toFixed(2)}%<br>` +
        `Weights:<br>` +
        minRiskWeights.map((w, i) => `${assets[i].name}: ${(w * 100).toFixed(2)}%`).join('<br>');

    const trace3 = {
        x: [minRisk],
        y: [targetReturns[minRiskIndex]],
        mode: 'markers',
        type: 'scatter',
        name: 'Minimum Variance Portfolio',
        marker: { color: 'green', size: 12 },
        text: [minRiskHoverText], // ホバー時のテキスト
        hoverinfo: 'text'
    };

    // 効用最大化ポートフォリオのホバーテキスト
    const maxUtilityHoverText = 
        `Maximum Utility Portfolio<br>` +
        `Expected Return: ${(maxUtilityReturn * 100).toFixed(2)}%<br>` +
        `Risk: ${(maxUtilityRisk * 100).toFixed(2)}%<br>` +
        `Weights:<br>` +
        maxUtilityWeights.map((w, i) => `${assets[i].name}: ${(w * 100).toFixed(2)}%`).join('<br>');

    const trace4 = {
        x: [maxUtilityRisk],
        y: [maxUtilityReturn],
        mode: 'markers',
        type: 'scatter',
        name: 'Maximum Utility Portfolio',
        marker: { color: 'orange', size: 12 },
        text: [maxUtilityHoverText], // ホバー時のテキスト
        hoverinfo: 'text'
    };

    const layout = {
        title: 'Efficient Frontier',
        xaxis: { title: 'Risk (Standard Deviation)' },
        yaxis: { title: 'Expected Return' },
        width: 800,
        height: 600
    };


    Plotly.newPlot('plot', [trace, trace2, trace3, trace4], layout);
}
  1. 効率的フロンティア(trace1)のプロット
    – 横軸 (x) はリスク(標準偏差)。
    – 縦軸 (y) はターゲットリターン。
    – 線 (mode: 'lines') でフロンティアを描画します。

  2. 最適ポートフォリオ(trace2)のプロット
    – 最適ポートフォリオのリスクとリターンの点を赤いマーカーでプロットします。

  3. プロットのレンダリング
    Plotly.newPlot を使用して、効率的フロンティア(青い線)と最適ポートフォリオ(赤い点)を1つのグラフに描画します。


ポートフォリオ計算ボタンのクリック処理

// ポートフォリオ計算
document.getElementById('calculatePortfolio').addEventListener('click', () => {
    if (assets.length < 2) {
        alert('少なくとも2つの資産を追加してください');
        return;
    }

    covarianceMatrix = calculateCovarianceMatrix();



    // 効率的フロンティアを描画
    plotEfficientFrontier();

});

コードのまとめ

このコード全体では、次の手順で効率的フロンティアと最適ポートフォリオを計算および可視化します:
1. 資産リストとその特性(リターン、リスク、相関係数)を入力。
2. 相関行列から共分散行列を計算。
3. ターゲットリターンごとにリスクを計算。
4. 効率的フロンティアをプロット。
5. 最大効用に基づく最適ポートフォリオを特定して強調表示。

タイトルとURLをコピーしました