/**
* @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();
}