parallel.es
Version:
Simple parallelization for EcmaScript
189 lines (188 loc) • 8.83 kB
JavaScript
var index_1 = require("../src/browser/index");
function initializeOptions(options) {
return Object.assign({}, {
investmentAmount: 1000000,
liquidity: 10000,
numRuns: 10000,
numYears: 10,
performance: 0,
projects: [],
seed: undefined,
volatility: 0.01
}, options);
}
function createMonteCarloEnvironment(options) {
/**
* Performs the monte carlo simulation for all years and num runs.
* @param cashFlows the cash flows
* @returns {number[][]} the simulated outcomes grouped by year
*/
function simulateOutcomes(cashFlows, numYears) {
function toAbsoluteIndices(indices) {
var currentPortfolioValue = options.investmentAmount;
var previousYearIndex = 100;
for (var relativeYear = 0; relativeYear < indices.length; ++relativeYear) {
var currentYearIndex = indices[relativeYear];
var cashFlowStartOfYear = relativeYear === 0 ? 0 : cashFlows[relativeYear - 1];
// scale current value with performance gain according to index
var performance_1 = currentYearIndex / previousYearIndex;
currentPortfolioValue = (currentPortfolioValue + cashFlowStartOfYear) * performance_1;
indices[relativeYear] = Math.round(currentPortfolioValue);
previousYearIndex = currentYearIndex;
}
return indices;
}
var result = new Array(options.numYears);
for (var year = 0; year <= numYears; ++year) {
result[year] = new Array(options.numRuns);
}
for (var run = 0; run < options.numRuns; run++) {
var indices = [100];
for (var i = 1; i <= numYears; i++) {
// const randomPerformance = 1 + random.normal(options.performance, options.volatility);
var randomPerformance = 1 + Math.random();
indices.push(indices[i - 1] * randomPerformance);
}
// convert the relative values from above to absolute values.
toAbsoluteIndices(indices);
for (var year = 0; year < indices.length; ++year) {
result[year][run] = indices[year];
}
}
return result;
}
function projectsToCashFlows() {
var cashFlows = [];
for (var year = 0; year < options.numYears; ++year) {
var projectsByThisYear = projectsByStartYear[year] || [];
var cashFlow = -projectsByThisYear.reduce(function (memo, project) { return memo + project.totalAmount; }, 0);
cashFlows.push(cashFlow);
}
return cashFlows;
}
function calculateNoInterestReferenceLine(cashFlows) {
var noInterestReferenceLine = [];
var investmentAmountLeft = options.investmentAmount;
for (var year = 0; year < options.numYears; ++year) {
investmentAmountLeft = investmentAmountLeft + cashFlows[year];
noInterestReferenceLine.push(investmentAmountLeft);
}
return noInterestReferenceLine;
}
var projectsToSimulate = options.projects;
if (options.taskIndex && options.valuesPerWorker) {
projectsToSimulate = options.projects.slice(options.taskIndex * options.valuesPerWorker, (options.taskIndex + 1) * options.valuesPerWorker);
}
var projects = options.projects.sort(function (a, b) { return a.startYear - b.startYear; });
// Group projects by startYear, use lodash groupBy instead
var projectsByStartYear = {};
for (var i = 0; i < projects.length; ++i) {
var project = projects[i];
var arr = projectsByStartYear[project.startYear] = projectsByStartYear[project.startYear] || [];
arr.push(project);
}
var cashFlows = projectsToCashFlows();
var noInterestReferenceLine = calculateNoInterestReferenceLine(cashFlows);
var numYears = projectsToSimulate.reduce(function (memo, project) { return Math.max(memo, project.startYear); }, 0);
return {
investmentAmount: options.investmentAmount,
liquidity: options.liquidity,
noInterestReferenceLine: noInterestReferenceLine,
numRuns: options.numRuns,
numYears: numYears,
projectsByStartYear: projectsByStartYear,
simulatedValues: simulateOutcomes(cashFlows, numYears)
};
}
function calculateProject(project, environment) {
var NUMBER_OF_BUCKETS = 10;
function groupForValue(value, groups) {
return groups.find(function (group) { return (typeof group.from === "undefined" || group.from <= value) && (typeof group.to === "undefined" || group.to > value); });
}
function createGroups(requiredAmount, noInterestReference) {
return [
{ description: "Ziel erreichbar", from: requiredAmount, name: "green", percentage: 0, separator: true },
{ description: "mit Zusatzliquidität erreichbar", from: requiredAmount - environment.liquidity, name: "yellow", percentage: 0, separator: true, to: requiredAmount },
{ description: "nicht erreichbar", from: noInterestReference, name: "gray", percentage: 0, separator: false, to: requiredAmount - environment.liquidity },
{ description: "nicht erreichbar, mit Verlust", name: "red", percentage: 0, separator: false, to: noInterestReference }
];
}
function calculateRequiredAmount() {
var amount = project.totalAmount;
var projectsSameYear = environment.projectsByStartYear[project.startYear];
for (var i = 0; i < projectsSameYear.length; ++i) {
var otherProject = projectsSameYear[i];
if (otherProject === project) {
break;
}
amount += otherProject.totalAmount;
}
return amount;
}
function median(values) {
var half = Math.floor(values.length / 2);
if (values.length % 2) {
return values[half];
}
return (values[half - 1] + values[half]) / 2.0;
}
var requiredAmount = calculateRequiredAmount();
var simulatedValuesThisYear = environment.simulatedValues[project.startYear];
simulatedValuesThisYear.sort(function (a, b) { return a - b; });
var groups = createGroups(requiredAmount, environment.noInterestReferenceLine[project.startYear]);
var valuesByGroup = {};
var bucketSize = Math.round(simulatedValuesThisYear.length / NUMBER_OF_BUCKETS);
var buckets = [];
for (var i = 0; i < simulatedValuesThisYear.length; i += bucketSize) {
var bucket = {
max: Number.MIN_VALUE,
min: Number.MAX_VALUE,
subBuckets: {}
};
for (var j = i; j < i + bucketSize; ++j) {
var value = simulatedValuesThisYear[j];
bucket.min = Math.min(bucket.min, value);
bucket.max = Math.max(bucket.max, value);
var group = groupForValue(simulatedValuesThisYear[j], groups);
valuesByGroup[group.name] = (valuesByGroup[group.name] || 0) + 1;
var subBucket = bucket.subBuckets[group.name] = bucket.subBuckets[group.name] || { group: group.name, max: Number.MIN_VALUE, min: Number.MAX_VALUE };
subBucket.min = Math.min(subBucket.min, value);
subBucket.max = Math.max(subBucket.max, value);
}
buckets.push(bucket);
}
var nonEmptyGroups = groups.filter(function (group) { return !!valuesByGroup[group.name]; });
nonEmptyGroups.forEach(function (group) { return group.percentage = valuesByGroup[group.name] / simulatedValuesThisYear.length; });
var oneSixth = Math.round(simulatedValuesThisYear.length / 6);
return {
buckets: buckets,
groups: nonEmptyGroups,
max: simulatedValuesThisYear[simulatedValuesThisYear.length - 1],
median: median(simulatedValuesThisYear),
min: simulatedValuesThisYear[0],
project: project,
twoThird: {
max: simulatedValuesThisYear[simulatedValuesThisYear.length - oneSixth],
min: simulatedValuesThisYear[oneSixth]
}
};
}
function syncMonteCarlo(options) {
var environment = createMonteCarloEnvironment(initializeOptions(options));
var projects = [];
for (var _i = 0, _a = options.projects; _i < _a.length; _i++) {
var project = _a[_i];
projects.push(calculateProject(project, environment));
}
return projects;
}
exports.syncMonteCarlo = syncMonteCarlo;
function parallelMonteCarlo(userOptions) {
var options = initializeOptions(userOptions);
return index_1["default"]
.from(options.projects, { minValuesPerTask: 2 })
.inEnvironment(createMonteCarloEnvironment, options)
.map(calculateProject);
}
exports.parallelMonteCarlo = parallelMonteCarlo;
;