ポートフォリオシュミレーションGASロジックメモ。

2025-04-07 シュミレーションロジックを作ったのでその時のメモ

”銘柄シュミレーション”タブパラメーター

“シュミレーション”タブの値の一部が自動コピーされる。

 

初期単価 上昇時倍率 下落時倍率 初期単価以上時上昇確率% 初期単価未満時上昇確率% 最大日数
1000 1.2 0.8 30 60 500
変動倍率% 高値時下落確率%
安値時下落確率%

 

用は一銘柄の値動きをデジタル的にシュミレーションする。

このパラメーターだと500日。初期価格1000円より高いときは30%の確率で上がる。つまり70%の価格で下がる。

逆に1000円未満なら60%の確率で上がる。

上昇時、下落時ともに20%ほど変動する。これが毎日起こるイベント。

で、500日ほどのシュミレーションを表示する。

まあこれ見てわかる通り、結構やばい銘柄のシュミレーションだよね。

今回は初期投資額1000円だけど、7割の確率で下落ってんだから。

 

“シュミレーション”タブパラメーター

最大銘柄数 初期単価 初期投資銘柄数 初期買ロット数 上昇時倍率 下落時倍率 初期単価以上時上昇確率% 初期単価未満時上昇確率% 最終勝ち判定 最終負け判定 シミュレーション回数 最大日数 売却閾値(価格) 売却持株率% 倒産考慮=1 倒産判定
10 1000 10 3 1.2 0.8 30 60 150000 6000 100 500 1500 33 1 50
初期金額(円) 30000 変動倍率% 20 高値時下落確率% 安値時下落確率% 初期単価より何倍になったら売るか? 1.5 倒産基準倍率→ 20
実行(1)→ 0 解説(メモ欄) 70 40 判定倍率 5

 

こちらのシートは複数銘柄(”最大銘柄数”)のシュミレーションに対応する。

最大銘柄数
10

 

ここでは10銘柄。同じような銘柄にしか対応していない。
ここでは10銘柄で回すってことね。

初期単価 初期投資銘柄数 初期買ロット数
1000 10 3
初期金額(円) 30000

 

初期単価はここでは1000円。初期ロットは3ロット。つまり3000円。

で、初期投資銘柄数が10銘柄だから30000円。これが初期投資額。

売却閾値(価格) 売却持株率%
1500 33
初期単価より何倍になったら売るか? 1.5

 

売却閾値(価格)”つまりここでは1500円に上昇したら33%ほど売却。

でもって、利確して持っているCashが十分あったら最も安い銘柄に再投資される。

最終勝ち判定 最終負け判定
150000 6000
判定倍率 5

 

シュミレーションを実行して日数がたつにつれて、初期投資額の5倍、もしくは5分の1なったら勝ち、負けと、その時点で判定する。

 

最大日数
500

 

500日ほどのシュミレーションを、

 

シミュレーション回数
100

 

100回ほど実行する。

実行結果は、

— 全体結果 (サマリー) —
最終的な勝ち確率: 92.00%
最終的な負け確率: 4.00%
期間内に未決着確率: 4.00%

 

みたいに表示される。

 

倒産考慮=1 倒産判定
1 50
倒産基準倍率→ 20

 

これは倒産リスクを考慮したシュミレーション。

倒産考慮が1だったらこのシュミレーションが追加される。つまり、50円とかすごく低下している場合は倒産と判定して全損。以降この銘柄には投資できない。

以下コード。あってるかな?多分そんなにひどくはないと思う。
ほとんどGemini 2.5Proさんが作った感じ。

 

/**
 * @OnlyCurrentDoc
 */
// — グローバル設定 —
const PORTFOLIO_SHEET_NAME = ‘シミュレーション’; // パラメータと結果を出力するシート名
const SINGLE_STOCK_SHEET_NAME = ‘銘柄シュミレーション’; // 新しい個別銘柄シミュレーション用シート名
const PORTFOLIO_START_ROW_RESULTS = 11;      // ポートフォリオ結果を出力し始める行
const PORTFOLIO_TOP_SUMMARY_START_ROW = 6;  // ポートフォリオ全体結果サマリーを出力し始める行
const SINGLE_STOCK_PARAM_ROW = 2;        // 個別銘柄パラメータが入力されている行
const SINGLE_STOCK_START_ROW_RESULTS = 10; //
/**
 * シートが編集されたときに自動的に実行される関数 (シンプル トリガー)
 * 各シミュレーションシートの B4 セルの値が 1 になったら実行し、完了後に 0 に戻す。
 * 注意: スクリプト実行時間に30秒の制限あり。
 * @param {Object} e イベントオブジェクト
 */
function onEdit(e) {
  const editedRange = e.range; // 編集されたセル範囲
  const sheet = editedRange.getSheet(); // 編集されたシート
  const sheetName = sheet.getName(); // シート名
  const cellNotation = editedRange.getA1Notation(); // セル番地 (例: “B4”)
  const editedValue = e.value; // 編集後の値 (文字列の場合もあるので注意)
  // トリガー設定
  const TRIGGER_CELL = ‘B4’;
  const TRIGGER_VALUE = 1;
  const RESET_VALUE = 0;
  const ERROR_VALUE = ‘エラー’; // エラー時にセルに表示する値
  // B4セルが編集され、その値が数値の1になった場合のみ処理を実行
  if (cellNotation === TRIGGER_CELL && Number(editedValue) === TRIGGER_VALUE) {
    // — ポートフォリオシミュレーションのトリガー —
    if (sheetName === PORTFOLIO_SHEET_NAME) {
      SpreadsheetApp.getActiveSpreadsheet().toast(`ポートフォリオシミュレーションを開始します… (最大30秒)`, ‘トリガー実行’, 5);
      try {
        // ★★★ runPortfolioSimulation内の ui.showModalDialog は動作しないので注意 ★★★
        // 必要であれば runPortfolioSimulation から該当行を削除またはコメントアウトする
        runPortfolioSimulation(); // 実行
        SpreadsheetApp.getActiveSpreadsheet().toast(‘ポートフォリオシミュレーションが完了しました。’, ‘完了’, 5);
        editedRange.setValue(RESET_VALUE); // 成功したら B4 を 0 に戻す
      } catch (error) {
        Logger.log(`onEditからのポートフォリオ実行エラー: ${error}\n${error.stack}`);
        SpreadsheetApp.getActiveSpreadsheet().toast(`エラーが発生しました: ${error.message}`, ‘エラー’, -1); // -1 で長く表示
        try {
          editedRange.setValue(ERROR_VALUE); // B4 に “エラー” と表示
        } catch (setValueError) {
          Logger.log(`B4セルへのエラー値設定に失敗: ${setValueError}`);
        }
      }
    }
    // — 個別銘柄シミュレーションのトリガー —
    else if (sheetName === SINGLE_STOCK_SHEET_NAME) {
      SpreadsheetApp.getActiveSpreadsheet().toast(`個別銘柄シミュレーションを開始します… (最大30秒)`, ‘トリガー実行’, 5);
      try {
        // ★★★ runSingleStockSimulation内の ui.showModalDialog は動作しないので注意 ★★★
        runSingleStockSimulation(); // 実行
        SpreadsheetApp.getActiveSpreadsheet().toast(‘個別銘柄シミュレーションが完了しました。’, ‘完了’, 5);
        editedRange.setValue(RESET_VALUE); // 成功したら B4 を 0 に戻す
      } catch (error) {
        Logger.log(`onEditからの個別銘柄実行エラー: ${error}\n${error.stack}`);
        SpreadsheetApp.getActiveSpreadsheet().toast(`エラーが発生しました: ${error.message}`, ‘エラー’, -1);
        try {
          editedRange.setValue(ERROR_VALUE); // B4 に “エラー” と表示
        } catch (setValueError) {
          Logger.log(`B4セルへのエラー値設定に失敗: ${setValueError}`);
        }
      }
    }
    // — トリガー対象外のシートやセルの場合は何もしない —
  }
}
/**
 * スプレッドシートを開いたときにカスタムメニューを追加します。
 */
function onOpen() {
  SpreadsheetApp.getUi()
      .createMenu(‘シミュレーション’) // メニュー名を統合
      .addItem(‘ポートフォリオ実行’, ‘runPortfolioSimulation’)
      .addSeparator() // 区切り線
      .addItem(‘個別銘柄実行’, ‘runSingleStockSimulation’) // 新しいメニュー項目
      .addToUi();
}
// — ポートフォリオシミュレーション関連関数 —
/**
 * ポートフォリオシミュレーションを実行するメイン関数
 */
function runPortfolioSimulation() {
  const ui = SpreadsheetApp.getUi();
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(PORTFOLIO_SHEET_NAME); // シート名定数を使用
  if (!sheet) {
    ui.alert(`シート “${PORTFOLIO_SHEET_NAME}” が見つかりません。`);
    return;
  }
  // 古い結果をクリア
  const lastRow = sheet.getLastRow();
  if (lastRow >= PORTFOLIO_TOP_SUMMARY_START_ROW) {
    // サマリーと詳細結果エリアを含む可能性のある範囲をクリア
    const clearRowCountSummary = Math.max(0, lastRow – PORTFOLIO_TOP_SUMMARY_START_ROW + 1);
    if (clearRowCountSummary > 0) {
        // ▼▼▼ 変更点: クリア範囲を9列に拡大 ▼▼▼
        sheet.getRange(PORTFOLIO_TOP_SUMMARY_START_ROW, 1, clearRowCountSummary, 9).clearContent();
        // ▲▲▲ 変更点 ▲▲▲
    }
  }
   if (lastRow >= PORTFOLIO_START_ROW_RESULTS) {
     const clearRowCountDetails = Math.max(0, lastRow – PORTFOLIO_START_ROW_RESULTS + 1);
     if (clearRowCountDetails > 0) {
         // ▼▼▼ 変更点: クリア範囲を9列に拡大 ▼▼▼
         sheet.getRange(PORTFOLIO_START_ROW_RESULTS, 1, clearRowCountDetails, 9).clearContent();
         // ▲▲▲ 変更点 ▲▲▲
     }
   }
  SpreadsheetApp.flush();
  try {
    const params = readParameters(sheet); // 変更後の関数を使用
    if (!params) return;
   // ui.showModalDialog(HtmlService.createHtmlOutput(‘<p>ポートフォリオシミュレーションを実行中です…</p><p>パラメータによっては数分かかることがあります。</p>’), ‘処理中’);
    const results = executeSimulation(params); // 変更後の関数を使用
    displayResults(sheet, results, params.numSimulations); // displayResultsは変更なし (内部で列数は調整済み)
    //ui.alert(‘ポートフォリオシミュレーションが完了しました。’);
  } catch (e) {
    Logger.log(`ポートフォリオエラー発生: ${e.message}\n${e.stack}`);
    ui.alert(`ポートフォリオシミュレーション中にエラーが発生しました: ${e.message}`);
  }
}
/**
 * ポートフォリオ用のシートからシミュレーションパラメータを読み込みます。
 * (倒産関連パラメータを追加)
 */
function readParameters(sheet) {
  const ui = SpreadsheetApp.getUi();
  // ▼▼▼ 変更点: 読み込み範囲を P列まで拡大 ▼▼▼
  const data = sheet.getRange(‘A2:P2’).getValues()[0]; // ポートフォリオ用パラメータ範囲
  // ▲▲▲ 変更点 ▲▲▲
  const params = {
    maxStocks: parseInt(data[0]),
    initialPrice: parseFloat(data[1]),
    initialInvestStocks: parseInt(data[2]),
    initialUnits: parseInt(data[3]),
    upMultiplier: parseFloat(data[4]),
    downMultiplier: parseFloat(data[5]),
    probUpHigh: parseFloat(data[6]) / 100.0,
    probUpLow: parseFloat(data[7]) / 100.0,
    winTargetTotalValue: parseFloat(data[8]),
    lossThresholdTotalValue: parseFloat(data[9]),
    numSimulations: parseInt(data[10]),
    maxDays: parseInt(data[11]),
    sellThresholdPrice: parseFloat(data[12]),
    sellRate: parseFloat(data[13]) / 100.0,
    // ▼▼▼ 変更点: 倒産パラメータ追加 ▼▼▼
    considerBankruptcy: parseInt(data[14]), // O列
    bankruptcyThreshold: parseFloat(data[15]) // P列
    // ▲▲▲ 変更点 ▲▲▲
  };
   // ▼▼▼ 変更点: バリデーション追加 ▼▼▼
   let errorMessage = ”;
   if (isNaN(params.maxStocks) || params.maxStocks <= 0) errorMessage += ‘・最大銘柄数は正の整数で入力してください。\n’;
   if (isNaN(params.initialPrice) || params.initialPrice <= 0) errorMessage += ‘・初期単価は正の数で入力してください。\n’;
   if (isNaN(params.initialInvestStocks) || params.initialInvestStocks < 0 || params.initialInvestStocks > params.maxStocks) errorMessage += ‘・初期投資銘柄数は0以上、最大銘柄数以下で入力してください。\n’;
   if (isNaN(params.initialUnits) || params.initialUnits <= 0) errorMessage += ‘・初期買ロット数は正の整数で入力してください。\n’;
   if (isNaN(params.upMultiplier) || params.upMultiplier <= 0) errorMessage += ‘・上昇時倍率は正の数で入力してください。\n’;
   if (isNaN(params.downMultiplier) || params.downMultiplier <= 0) errorMessage += ‘・下落時倍率は正の数で入力してください。\n’;
   if (isNaN(params.probUpHigh) || params.probUpHigh < 0 || params.probUpHigh > 1) errorMessage += ‘・初期単価以上時上昇確率は0~100%で入力してください。\n’;
   if (isNaN(params.probUpLow) || params.probUpLow < 0 || params.probUpLow > 1) errorMessage += ‘・初期単価未満時上昇確率は0~100%で入力してください。\n’;
   const initialTotalValue = params.initialInvestStocks * params.initialPrice * params.initialUnits;
   if (isNaN(params.winTargetTotalValue) || params.winTargetTotalValue <= initialTotalValue) errorMessage += `・最終勝ち判定は初期総資産 (${initialTotalValue.toLocaleString()}) より大きい値を入力してください。\n`;
   if (isNaN(params.lossThresholdTotalValue) || params.lossThresholdTotalValue < 0 || params.lossThresholdTotalValue >= initialTotalValue) errorMessage += `・最終負け判定は0以上、初期総資産 (${initialTotalValue.toLocaleString()}) 未満の値を入力してください。\n`;
   if (isNaN(params.numSimulations) || params.numSimulations <= 0) errorMessage += ‘・シミュレーション回数は正の整数で入力してください。\n’;
   if (isNaN(params.maxDays) || params.maxDays <= 0) errorMessage += ‘・最大日数は正の整数で入力してください。\n’;
   if (isNaN(params.sellThresholdPrice) || params.sellThresholdPrice <= 0) errorMessage += ‘・売却閾値(価格)は正の数で入力してください。\n’;
   if (isNaN(params.sellRate) || params.sellRate < 0 || params.sellRate > 1) errorMessage += ‘・売却持株率は0~100%で入力してください。\n’;
   // — 倒産パラメータのバリデーション —
   if (isNaN(params.considerBankruptcy) || (params.considerBankruptcy !== 0 && params.considerBankruptcy !== 1)) errorMessage += ‘・倒産考慮は 0 (しない) または 1 (する) で入力してください。\n’;
   if (params.considerBankruptcy === 1 && (isNaN(params.bankruptcyThreshold) || params.bankruptcyThreshold < 0)) errorMessage += ‘・倒産考慮が1の場合、倒産判定価格は0以上の数で入力してください。\n’;
   // — ここまで倒産パラメータのバリデーション —
   if (errorMessage) {
     ui.alert(`ポートフォリオのパラメータ値が不正です。シート “${PORTFOLIO_SHEET_NAME}” のA2:P2を確認してください。\n${errorMessage}`);
     return null;
   }
   // ▲▲▲ 変更点 ▲▲▲
  Logger.log(‘読み込みポートフォリオパラメータ: %s’, JSON.stringify(params, null, 2));
  return params;
}
/**
 * ポートフォリオのモンテカルロシミュレーションを実行します。
 * (倒産処理を追加)
 * @param {object} params シミュレーションパラメータ
 * @return {object} シミュレーション結果 (winDayCounts, lossDayCounts, undecidedCount, investmentCounts, dailyAvgStockCount, dailyAvgCash)
 */
function executeSimulation(params) {
  const winDayCounts = {};
  const lossDayCounts = {};
  const investmentCounts = {};
  let totalStockCountPerDay = {};
  let totalCashPerDay = {};
  const dailyAvgStockCount = {};
  const dailyAvgCash = {};
  let undecidedCount = 0;
  const startTime = new Date();
  // ▼▼▼ 変更点: 倒産パラメータを params から取得 ▼▼▼
  const { considerBankruptcy, bankruptcyThreshold } = params;
  // ▲▲▲ 変更点 ▲▲▲
  for (let i = 0; i < params.numSimulations; i++) {
    if ((new Date().getTime() – startTime.getTime()) > 330000) { // タイムアウトチェックは5.5分
      Logger.log(`警告: ポートフォリオシミュレーションループが5.5分を超えました (イテレーション ${i})。早期終了します。`);
      undecidedCount += (params.numSimulations – i);
      break; // ループを抜ける
    }
    let portfolio = [];
    let cash = 0; // 各シミュレーション開始時にキャッシュをリセット
    let nextStockId = i * params.maxStocks * params.maxDays; // ID重複回避のための大まかなオフセット
    // — ポートフォリオ初期化 —
    const initialPortfolio = [];
    let currentNextStockId = 0;
    for (let j = 0; j < params.initialInvestStocks; j++) {
      // ▼▼▼ 変更点: isBankrupt プロパティを追加 ▼▼▼
      initialPortfolio.push({
          id: currentNextStockId++,
          price: params.initialPrice,
          units: params.initialUnits,
          isBankrupt: false // 倒産フラグ (初期値はfalse)
      });
      // ▲▲▲ 変更点 ▲▲▲
    }
    portfolio = initialPortfolio;
    nextStockId += currentNextStockId;
    let won = false;
    let lost = false;
    // — 日数ループ —
    for (let day = 1; day <= params.maxDays; day++) {
      let soldToday = false;
      let proceeds = 0.0;
      let dailyInvestmentActions = [];
      // a. 価格変動 と 倒産チェック
      portfolio.forEach(stock => {
        // ▼▼▼ 変更点: 倒産していない銘柄のみ処理 ▼▼▼
        if (stock.units > 0 && !stock.isBankrupt) {
          const probUp = stock.price < params.initialPrice ? params.probUpLow : params.probUpHigh;
          stock.price *= (Math.random() < probUp) ? params.upMultiplier : params.downMultiplier;
          // — ▼▼▼ 倒産チェック ▼▼▼ —
          if (considerBankruptcy === 1 && stock.price <= bankruptcyThreshold) {
            Logger.log(`Day ${day}, Sim ${i}: 銘柄 ID ${stock.id} が倒産しました (価格: ${stock.price.toFixed(2)} <= 閾値: ${bankruptcyThreshold})`);
            stock.price = 0; // 価値を0に
            stock.units = 0; // 保有数を0に(損失確定)
            stock.isBankrupt = true; // 倒産フラグを立てる
            // キャッシュには影響しない(価値が消滅するだけ)
          }
          // — ▲▲▲ 倒産チェック ▲▲▲ —
        }
        // ▲▲▲ 変更点 ▲▲▲
      });
      // b. 売却処理
      const stocksToProcess = […portfolio]; // イテレーション中の変更に対応するためコピー
      stocksToProcess.forEach(stock => {
         // ▼▼▼ 変更点: 倒産していない銘柄のみ売却対象 ▼▼▼
         if (!stock.isBankrupt && stock.units > 0 && stock.price >= params.sellThresholdPrice) {
            soldToday = true;
            let unitsToSell = (stock.units === 1) ? 1 : Math.max(1, Math.floor(stock.units * params.sellRate));
            // ポートフォリオ内の元の株を見つける (コピーではなく)
            const originalStock = portfolio.find(s => s.id === stock.id);
            if (originalStock) { // 見つかった場合のみ(通常は見つかるはず)
                const actuallySold = Math.min(unitsToSell, originalStock.units); // 売却可能数を超えないように
                originalStock.units -= actuallySold;
                // 売却益は売却時の株価で計算 (倒産チェック後なので0の可能性もあるが、閾値以上なので0ではないはず)
                proceeds += originalStock.price * actuallySold;
            }
         }
         // ▲▲▲ 変更点 ▲▲▲
      });
      cash += proceeds; // 売却益をキャッシュに加算
      // c. 投資処理
      if (soldToday && cash > 0) { // 売却があった場合のみ投資を試みる
        if (params.maxStocks === 1) {
          // — 最大銘柄数1の場合 (倒産していたら追加投資できない) —
          const targetStock = portfolio.find(s => s.id === 0);
          // ▼▼▼ 変更点: 倒産していないかチェック ▼▼▼
          if (targetStock && !targetStock.isBankrupt) {
          // ▲▲▲ 変更点 ▲▲▲
            const purchasePrice = targetStock.price; // 投資決定時の価格
            while (cash >= purchasePrice) {
               const currentTargetStock = portfolio.find(s => s.id === 0);
               // ▼▼▼ 変更点: 倒産していないか、ユニットがあるかチェック ▼▼▼
               if (!currentTargetStock || currentTargetStock.units <= 0 || currentTargetStock.isBankrupt) break;
               // ▲▲▲ 変更点 ▲▲▲
               currentTargetStock.units += 1;
               cash -= purchasePrice;
               dailyInvestmentActions.push({ type: ‘追加’, id: currentTargetStock.id });
            }
          }
        } else {
          // — 最大銘柄数2以上の場合 —
          // c-1. 新規投資 (空きがあれば)
          let activeStocks = portfolio.filter(s => s.units > 0); // 倒産銘柄は units = 0 なので含まれない
          let availableSlots = params.maxStocks – activeStocks.length;
          const newInvestmentCost = params.initialPrice * params.initialUnits;
          while (availableSlots > 0 && cash >= newInvestmentCost) {
             // ▼▼▼ 変更点: isBankrupt プロパティを追加 ▼▼▼
             const newStock = { id: nextStockId++, price: params.initialPrice, units: params.initialUnits, isBankrupt: false };
             // ▲▲▲ 変更点 ▲▲▲
             portfolio.push(newStock);
             cash -= newInvestmentCost;
             availableSlots–; // 空きスロットを減らす
             dailyInvestmentActions.push({ type: ‘新規’, id: newStock.id });
          }
          // c-2. 追加投資 (現金が残っていれば)
          if (cash > 0) {
              while (cash > 0) {
                  // ▼▼▼ 変更点: 倒産しておらずユニットを持つ銘柄のみ対象 ▼▼▼
                  let investableStocks = portfolio.filter(s => s.units > 0 && !s.isBankrupt);
                  // ▲▲▲ 変更点 ▲▲▲
                  if (investableStocks.length === 0) break; // 投資対象がなければループ終了
                  // 最も価格が安い銘柄に追加投資 (価格が同じ場合はIDが若い方)
                  investableStocks.sort((a, b) => a.price – b.price || a.id – b.id);
                  const cheapestStockRef = investableStocks[0]; // フィルタ後の配列の要素
                  const originalStock = portfolio.find(s => s.id === cheapestStockRef.id); // 元のポートフォリオから検索
                  // 買えるかチェックして購入 (倒産チェック済み)
                  if (originalStock && cash >= originalStock.price && originalStock.price > 0) { // 価格が0より大きいことも念のため確認
                     const purchasePrice = originalStock.price; // 購入時の価格
                     originalStock.units += 1;
                     cash -= purchasePrice;
                     dailyInvestmentActions.push({ type: ‘追加’, id: originalStock.id });
                  } else {
                      // 最も安い株すら買えない or 価格が0 or 見つからない
                      break;
                  }
              } // while (cash > 0) end
          } // if (cash > 0) end for 追加投資
        } // else (maxStocks > 1) end
      } // End of investment logic
      // — 日毎の投資アクションを集計 — (変更なし)
      if (dailyInvestmentActions.length > 0) {
          if (!investmentCounts[day]) {
              investmentCounts[day] = {};
          }
          dailyInvestmentActions.forEach(action => {
              const logKey = `${action.type}(ID${action.id})`;
              investmentCounts[day][logKey] = (investmentCounts[day][logKey] || 0) + 1;
          });
      }
      // — d. 日毎の保有銘柄数記録 — (変更なし、units > 0 でフィルタするので倒産銘柄は除かれる)
      const currentStockCount = portfolio.filter(s => s.units > 0).length;
      totalStockCountPerDay[day] = (totalStockCountPerDay[day] || 0) + currentStockCount;
      // — e. 日毎のキャッシュ記録 — (変更なし)
      totalCashPerDay[day] = (totalCashPerDay[day] || 0) + cash;
      // — f. 総資産価値計算 — (変更なし、倒産銘柄は price = 0, units = 0 なので影響なし)
      const currentStockValue = portfolio.reduce((sum, stock) => sum + stock.price * stock.units, 0);
      const totalValue = currentStockValue + cash;
      // — g. 勝敗判定 — (変更なし)
      if (totalValue >= params.winTargetTotalValue) {
        winDayCounts[day] = (winDayCounts[day] || 0) + 1;
        won = true;
        break;
      } else if (totalValue <= params.lossThresholdTotalValue) {
        lossDayCounts[day] = (lossDayCounts[day] || 0) + 1;
        lost = true;
        break;
      }
    } // day loop end
    // — シミュレーションが日数上限まで達した場合 (未決着) — (変更なし)
    if (!won && !lost) {
      undecidedCount++;
    }
  } // simulation loop end
  // — 平均計算 — (変更なし)
  const simulatedStockDaysKeys = Object.keys(totalStockCountPerDay);
  let maxStockDayReached = simulatedStockDaysKeys.length > 0 ? Math.max(…simulatedStockDaysKeys.map(Number)) : 0;
  const simulatedCashDaysKeys = Object.keys(totalCashPerDay);
  let maxCashDayReached = simulatedCashDaysKeys.length > 0 ? Math.max(…simulatedCashDaysKeys.map(Number)) : 0;
  const maxDayOverall = Math.max(maxStockDayReached, maxCashDayReached, params.maxDays); // 考慮する最大日数をパラメータのmaxDaysも含める
  for (let d = 1; d <= maxDayOverall; d++) {
       dailyAvgStockCount[d] = (totalStockCountPerDay[d] || 0) / params.numSimulations;
       dailyAvgCash[d] = (totalCashPerDay[d] || 0) / params.numSimulations;
  }
  const endTime = new Date();
  Logger.log(`Portfolio Simulation finished in ${(endTime.getTime() – startTime.getTime()) / 1000} seconds.`);
  return { winDayCounts, lossDayCounts, undecidedCount, investmentCounts, dailyAvgStockCount, dailyAvgCash };
}
/**
 * ポートフォリオシミュレーション結果をシートに出力します。
 * (この関数のロジック自体は変更なし、表示列数は元々dailyAvgCash追加で9列対応済み)
 * @param {GoogleAppsScript.Spreadsheet.Sheet} sheet 結果を出力するシート
 * @param {object} results executeSimulationからの結果オブジェクト
 * @param {number} numSimulations シミュレーションの総回数
 */
function displayResults(sheet, results, numSimulations) {
  const { winDayCounts, lossDayCounts, undecidedCount, investmentCounts, dailyAvgStockCount, dailyAvgCash } = results;
  const outputData = [];
  const MAX_LOG_LENGTH = 250; // G列の文字数制限
  // — 最終結果の計算 —
  const totalWins = Object.values(winDayCounts).reduce((sum, count) => sum + count, 0);
  const totalLosses = Object.values(lossDayCounts).reduce((sum, count) => sum + count, 0);
  const finalWinProb = (numSimulations > 0) ? totalWins / numSimulations : 0;
  const finalLossProb = (numSimulations > 0) ? totalLosses / numSimulations : 0;
  const actualUndecidedCount = numSimulations – totalWins – totalLosses;
  const finalUndecidedProb = (numSimulations > 0) ? actualUndecidedCount / numSimulations : 0;
  // — 上部サマリー出力 —
  sheet.getRange(PORTFOLIO_TOP_SUMMARY_START_ROW, 1, 4, 2).clearContent();
  sheet.getRange(PORTFOLIO_TOP_SUMMARY_START_ROW, 1).setValue(‘— 全体結果 (サマリー) —‘);
  sheet.getRange(PORTFOLIO_TOP_SUMMARY_START_ROW + 1, 1).setValue(‘最終的な勝ち確率:’);
  sheet.getRange(PORTFOLIO_TOP_SUMMARY_START_ROW + 1, 2).setValue(finalWinProb).setNumberFormat(‘0.00%’);
  sheet.getRange(PORTFOLIO_TOP_SUMMARY_START_ROW + 2, 1).setValue(‘最終的な負け確率:’);
  sheet.getRange(PORTFOLIO_TOP_SUMMARY_START_ROW + 2, 2).setValue(finalLossProb).setNumberFormat(‘0.00%’);
  if (actualUndecidedCount > 0 || finalUndecidedProb > 0) {
       sheet.getRange(PORTFOLIO_TOP_SUMMARY_START_ROW + 3, 1).setValue(‘期間内に未決着確率:’);
       sheet.getRange(PORTFOLIO_TOP_SUMMARY_START_ROW + 3, 2).setValue(finalUndecidedProb).setNumberFormat(‘0.00%’);
  }
  // — 日次結果の作成 —
  outputData.push([‘日数’, ‘勝ち確率(日次)’, ‘負け確率(日次)’, ‘継続中確率’, ‘累計勝ち確率’, ‘累計負け確率’, ‘投資回数 (ID別抜粋)’, ‘平均保有銘柄数’, ‘平均余剰Cash’]);
  let cumulativeWinProb = 0.0;
  let cumulativeLossProb = 0.0;
  let runningProb = 1.0;
  // 結果が存在する全ての日数をリストアップ
  const allWinLossDays = new Set([…Object.keys(winDayCounts).map(Number), …Object.keys(lossDayCounts).map(Number)]);
  const allAvgCountDays = Object.keys(dailyAvgStockCount).map(Number);
  const allInvestmentDays = Object.keys(investmentCounts).map(Number);
  const allAvgCashDays = Object.keys(dailyAvgCash).map(Number);
  const allRelevantDays = new Set([…allWinLossDays, …allAvgCountDays, …allInvestmentDays, …allAvgCashDays]);
  // 結果がある最大日数と、パラメータの最大日数のうち大きい方をシミュレーション期間とする
  const maxDayOccurred = allRelevantDays.size > 0 ? Math.max(…allRelevantDays) : 0;
  const simulationDuration = Math.max(1, maxDayOccurred); // executeSimulation内で maxDays まで計算するようにしたのでこれで良いはず
  if (numSimulations > 0) {
      for (let day = 1; day <= simulationDuration; day++) {
          const dailyWinCount = winDayCounts[day] || 0;
          const dailyLossCount = lossDayCounts[day] || 0;
          const dailyWinProb = dailyWinCount / numSimulations;
          const dailyLossProb = dailyLossCount / numSimulations;
          let investmentInfo = ”;
          if (investmentCounts[day]) {
              const counts = investmentCounts[day];
              const logEntries = Object.entries(counts)
                                      .sort(([, countA], [, countB]) => countB – countA)
                                      .map(([key, count]) => `${key}:${count}回`);
              investmentInfo = logEntries.join(‘, ‘);
              if (investmentInfo.length > MAX_LOG_LENGTH) {
                investmentInfo = investmentInfo.substring(0, MAX_LOG_LENGTH) + “…”;
              }
          }
          const prevRunningProb = runningProb;
          cumulativeWinProb += dailyWinProb;
          cumulativeLossProb += dailyLossProb;
          runningProb = Math.max(0, prevRunningProb – dailyWinProb – dailyLossProb);
          const avgStockCount = dailyAvgStockCount[day] !== undefined ? dailyAvgStockCount[day] : (day === 1 ? params.initialInvestStocks : 0); // 初日は初期値、以降は計算結果か0
          const avgCash = dailyAvgCash[day] !== undefined ? dailyAvgCash[day] : 0;
          outputData.push([
              day,
              dailyWinProb,
              dailyLossProb,
              prevRunningProb,
              cumulativeWinProb,
              cumulativeLossProb,
              investmentInfo,
              avgStockCount,
              avgCash
          ]);
      }
  } else {
       outputData.push([1, 0, 0, 1.0, 0, 0, ‘シミュレーション未実行’, 0, 0]);
       Logger.log(“シミュレーション回数が0のため、詳細結果はありません。”);
  }
   // — 日次結果をシートに出力 —
  if (outputData.length > 1) {
    const outputRange = sheet.getRange(PORTFOLIO_START_ROW_RESULTS, 1, outputData.length, 9);
    outputRange.setValues(outputData);
    const dataRows = outputData.length – 1;
    sheet.getRange(PORTFOLIO_START_ROW_RESULTS + 1, 2, dataRows , 5).setNumberFormat(‘0.00%’); // B-F列: 確率
    sheet.getRange(PORTFOLIO_START_ROW_RESULTS + 1, 1, dataRows , 1).setNumberFormat(‘0’);      // A列: 日数
    sheet.getRange(PORTFOLIO_START_ROW_RESULTS + 1, 7, dataRows , 1).setNumberFormat(‘@’);      // G列: テキスト
    sheet.getRange(PORTFOLIO_START_ROW_RESULTS + 1, 8, dataRows , 1).setNumberFormat(‘0.00’);   // H列: 平均銘柄数
    sheet.getRange(PORTFOLIO_START_ROW_RESULTS + 1, 9, dataRows , 1).setNumberFormat(‘#,##0.00’); // I列: 平均Cash
  } else {
    sheet.getRange(PORTFOLIO_START_ROW_RESULTS, 1, 1, 9).setValues(outputData); // ヘッダーのみ
    sheet.getRange(PORTFOLIO_START_ROW_RESULTS + 1, 1).setValue(“シミュレーション結果がありません。”);
  }
   // — 最終結果をシート下部に出力 —
   const finalResultStartRow = Math.max(PORTFOLIO_START_ROW_RESULTS + outputData.length, sheet.getLastRow()) + 2; // 結果の下に2行空ける
   sheet.getRange(finalResultStartRow, 1, 4, 2).clearContent(); // 既存の最終結果をクリア
   sheet.getRange(finalResultStartRow, 1).setValue(‘— 最終結果 —‘);
   sheet.getRange(finalResultStartRow + 1, 1).setValue(‘最終的な勝ち確率:’);
   sheet.getRange(finalResultStartRow + 1, 2).setValue(finalWinProb).setNumberFormat(‘0.00%’);
   sheet.getRange(finalResultStartRow + 2, 1).setValue(‘最終的な負け確率:’);
   sheet.getRange(finalResultStartRow + 2, 2).setValue(finalLossProb).setNumberFormat(‘0.00%’);
   if (actualUndecidedCount > 0 || finalUndecidedProb > 0) {
        sheet.getRange(finalResultStartRow + 3, 1).setValue(‘期間内に未決着確率:’);
        sheet.getRange(finalResultStartRow + 3, 2).setValue(finalUndecidedProb).setNumberFormat(‘0.00%’);
   }
   SpreadsheetApp.flush(); // シートへの変更を即時反映
}
// — ここから個別銘柄シミュレーション用関数 —
// (個別銘柄シミュレーションには倒産の概念は今回追加しないため、変更なし)
/**
 * 個別銘柄シミュレーションを実行するメイン関数
 */
function runSingleStockSimulation() {
  const ui = SpreadsheetApp.getUi();
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SINGLE_STOCK_SHEET_NAME);
  if (!sheet) {
    ui.alert(`シート “${SINGLE_STOCK_SHEET_NAME}” が見つかりません。`);
    return;
  }
  // 古い結果をクリア (ヘッダー行は残す)
  const lastRow = sheet.getLastRow();
  if (lastRow >= SINGLE_STOCK_START_ROW_RESULTS) {
    sheet.getRange(SINGLE_STOCK_START_ROW_RESULTS, 1, lastRow – SINGLE_STOCK_START_ROW_RESULTS + 1, 2).clearContent();
  }
  SpreadsheetApp.flush();
  try {
    const params = readSingleStockParameters(sheet);
    if (!params) return; // パラメータ読み込み失敗
  //  ui.showModalDialog(HtmlService.createHtmlOutput(‘<p>個別銘柄シミュレーションを実行中です…</p>’), ‘処理中’);
    const results = executeSingleStockSimulation(params);
    displaySingleStockResults(sheet, results);
   // ui.alert(‘個別銘柄シミュレーションが完了しました。’);
  } catch (e) {
    Logger.log(`個別銘柄エラー発生: ${e.message}\n${e.stack}`);
    ui.alert(`個別銘柄シミュレーション中にエラーが発生しました: ${e.message}`);
  }
}
/**
 * シート「銘柄シュミレーション」からパラメータを読み込みます。
 * @param {GoogleAppsScript.Spreadsheet.Sheet} sheet パラメータが書かれたシート
 * @return {object|null} パラメータオブジェクト、またはエラー時に null
 */
function readSingleStockParameters(sheet) {
  const ui = SpreadsheetApp.getUi();
  const dataRange = sheet.getRange(SINGLE_STOCK_PARAM_ROW, 1, 1, 6);
  const data = dataRange.getValues()[0];
  const params = {
    initialPrice: parseFloat(data[0]),     // A列: 初期単価
    upMultiplier: parseFloat(data[1]),     // B列: 上昇時倍率
    downMultiplier: parseFloat(data[2]),   // C列: 下落時倍率
    probUpHigh: parseFloat(data[3]) / 100.0, // D列: 初期単価以上時上昇確率% -> 確率(0-1)
    probUpLow: parseFloat(data[4]) / 100.0,  // E列: 初期単価未満時上昇確率% -> 確率(0-1)
    maxDays: parseInt(data[5])             // F列: 最大日数
  };
  if (isNaN(params.initialPrice) || params.initialPrice <= 0 ||
      isNaN(params.upMultiplier) || params.upMultiplier <= 0 ||
      isNaN(params.downMultiplier) || params.downMultiplier <= 0 ||
      isNaN(params.probUpHigh) || params.probUpHigh < 0 || params.probUpHigh > 1 ||
      isNaN(params.probUpLow) || params.probUpLow < 0 || params.probUpLow > 1 ||
      isNaN(params.maxDays) || params.maxDays <= 0) {
    ui.alert(`パラメータの値が不正です。シート “${SINGLE_STOCK_SHEET_NAME}” の ${SINGLE_STOCK_PARAM_ROW}行目 (A列からF列) を確認してください。`);
    return null;
  }
  Logger.log(‘読み込み個別銘柄パラメータ: %s’, JSON.stringify(params, null, 2));
  return params;
}
/**
 * 一銘柄の価格変動シミュレーションを1回実行します。
 * @param {object} params シミュレーションパラメータ (readSingleStockParameters から取得)
 * @return {Array<Array<number>>} 日ごとの結果 [[日数, 価格], [日数, 価格], …] の配列
 */
function executeSingleStockSimulation(params) {
  const dailyResults = [];
  let currentPrice = params.initialPrice;
  dailyResults.push([0, currentPrice]);
  for (let day = 1; day <= params.maxDays; day++) {
    const isAboveInitial = currentPrice >= params.initialPrice;
    const probabilityUp = isAboveInitial ? params.probUpHigh : params.probUpLow;
    if (Math.random() < probabilityUp) {
      currentPrice *= params.upMultiplier;
    } else {
      currentPrice *= params.downMultiplier;
    }
    dailyResults.push([day, currentPrice]);
  }
  return dailyResults;
}
/**
 * 個別銘柄シミュレーション結果をシートに出力します。
 * @param {GoogleAppsScript.Spreadsheet.Sheet} sheet 結果を出力するシート
 * @param {Array<Array<number>>} results executeSingleStockSimulationからの結果配列 [[日数, 価格], …]
 */
function displaySingleStockResults(sheet, results) {
  if (!results || results.length === 0) {
    sheet.getRange(SINGLE_STOCK_START_ROW_RESULTS, 1).setValue(“シミュレーション結果がありません。”);
    return;
  }
  const headerRow = SINGLE_STOCK_START_ROW_RESULTS – 1;
  sheet.getRange(headerRow, 1, 1, 2).setValues([[‘日数’, ‘価格’]]);
  const outputRange = sheet.getRange(SINGLE_STOCK_START_ROW_RESULTS, 1, results.length, 2);
  outputRange.setValues(results);
  sheet.getRange(SINGLE_STOCK_START_ROW_RESULTS, 1, results.length, 1).setNumberFormat(‘0’);
  sheet.getRange(SINGLE_STOCK_START_ROW_RESULTS, 2, results.length, 1).setNumberFormat(‘#,##0.00’);
  SpreadsheetApp.flush();
}

 

 

 

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です