parallel.es
Version:
Simple parallelization for EcmaScript
341 lines (281 loc) • 11.5 kB
text/typescript
import parallel from "../src/browser/index";
import {Dictionary} from "lodash";
/* tslint:disable:no-var-requires */
// declare function require(name: string): any;
// const Random = require("simjs-random");
// const random = new Random(10);
export interface IProject {
startYear: number;
totalAmount: number;
}
interface IBucket {
min: number;
max: number;
subBuckets: { [groupName: string]: { group: string; min: number, max: number } };
}
interface IGroup {
/**
* The unique name of this group
*/
name: string;
/**
* The description of the group
*/
description: string;
/**
* Should a separator line been drawn for this group?
*/
separator: boolean;
/**
* Whats the percentage of values in this group to the total number of simulated values
*/
percentage: number;
/**
* Whats the minimum value that is still part of this group
*/
from?: number;
/**
* Whats the maximum value (exclusive) that defines the upper end of this group
*/
to?: number;
}
export interface IProjectResult {
/**
* The minimal simulated value for this project
*/
min: number;
/**
* The maximal simulated value
*/
max: number;
/** The median of the values found for this project
*/
median: number;
/**
* Defines where the 2/3 of the simulated values start / end.
*/
twoThird: {
min: number;
max: number;
};
buckets: IBucket[];
groups: IGroup[];
/**
* The project
*/
project: IProject;
}
interface IMonteCarloEnvironment {
investmentAmount: number;
liquidity: number;
noInterestReferenceLine: number[];
numRuns: number;
numYears: number;
projectsByStartYear: Dictionary<IProject[]>;
simulatedValues: number[][];
}
export interface IMonteCarloSimulationOptions {
numYears?: number;
numRuns?: number;
projects?: IProject[];
investmentAmount?: number;
performance?: number;
seed?: number;
volatility: number;
liquidity?: number;
}
interface IInitializedMonteCarloSimulationOptions {
numYears: number;
numRuns: number;
projects: IProject[];
investmentAmount: number;
performance: number;
seed?: number;
taskIndex?: number;
valuesPerWorker?: number;
liquidity: number;
volatility: number;
}
function initializeOptions(options?: IMonteCarloSimulationOptions): IInitializedMonteCarloSimulationOptions {
return Object.assign({}, {
investmentAmount: 1000000,
liquidity: 10000,
numRuns: 10000,
numYears: 10,
performance: 0,
projects: [],
seed: undefined,
volatility: 0.01
}, options);
}
function createMonteCarloEnvironment(options: IInitializedMonteCarloSimulationOptions): IMonteCarloEnvironment {
/**
* 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: number[], numYears: number): number[][] {
function toAbsoluteIndices(indices: number[]) {
let currentPortfolioValue = options.investmentAmount;
let previousYearIndex = 100;
for (let relativeYear = 0; relativeYear < indices.length; ++relativeYear) {
const currentYearIndex = indices[relativeYear];
const cashFlowStartOfYear = relativeYear === 0 ? 0 : cashFlows[relativeYear - 1];
// scale current value with performance gain according to index
const performance = currentYearIndex / previousYearIndex;
currentPortfolioValue = (currentPortfolioValue + cashFlowStartOfYear) * performance;
indices[relativeYear] = Math.round(currentPortfolioValue);
previousYearIndex = currentYearIndex;
}
return indices;
}
const result: number[][] = new Array(options.numYears);
for (let year = 0; year <= numYears; ++year) {
result[year] = new Array(options.numRuns);
}
for (let run = 0; run < options.numRuns; run++) {
const indices = [100];
for (let i = 1; i <= numYears; i++) {
// const randomPerformance = 1 + random.normal(options.performance, options.volatility);
const randomPerformance = 1 + Math.random();
indices.push(indices[i - 1] * randomPerformance);
}
// convert the relative values from above to absolute values.
toAbsoluteIndices(indices);
for (let year = 0; year < indices.length; ++year) {
result[year][run] = indices[year];
}
}
return result;
}
function projectsToCashFlows() {
const cashFlows: number[] = [];
for (let year = 0; year < options.numYears; ++year) {
const projectsByThisYear = projectsByStartYear[year] || [];
const cashFlow = -projectsByThisYear.reduce((memo, project) => memo + project.totalAmount, 0);
cashFlows.push(cashFlow);
}
return cashFlows;
}
function calculateNoInterestReferenceLine(cashFlows: number[]) {
const noInterestReferenceLine: number[] = [];
let investmentAmountLeft = options.investmentAmount;
for (let year = 0; year < options.numYears; ++year) {
investmentAmountLeft = investmentAmountLeft + cashFlows[year];
noInterestReferenceLine.push(investmentAmountLeft);
}
return noInterestReferenceLine;
}
let projectsToSimulate: IProject[] = options.projects;
if (options.taskIndex && options.valuesPerWorker) {
projectsToSimulate = options.projects.slice(options.taskIndex * options.valuesPerWorker, (options.taskIndex + 1) * options.valuesPerWorker);
}
const projects = options.projects.sort((a, b) => a.startYear - b.startYear);
// Group projects by startYear, use lodash groupBy instead
const projectsByStartYear: Dictionary<IProject[]> = {};
for (let i = 0; i < projects.length; ++i) {
const project = projects[i];
const arr = projectsByStartYear[project.startYear] = projectsByStartYear[project.startYear] || [];
arr.push(project);
}
const cashFlows = projectsToCashFlows();
const noInterestReferenceLine = calculateNoInterestReferenceLine(cashFlows);
const numYears = projectsToSimulate.reduce((memo, project) => Math.max(memo, project.startYear), 0);
return {
investmentAmount: options.investmentAmount,
liquidity: options.liquidity,
noInterestReferenceLine,
numRuns: options.numRuns,
numYears,
projectsByStartYear,
simulatedValues: simulateOutcomes(cashFlows, numYears)
};
}
function calculateProject(project: IProject, environment: IMonteCarloEnvironment): IProjectResult {
const NUMBER_OF_BUCKETS = 10;
function groupForValue(value: number, groups: IGroup[]): IGroup {
return groups.find(group => (typeof group.from === "undefined" || group.from <= value) && (typeof group.to === "undefined" || group.to > value))!;
}
function createGroups(requiredAmount: number, noInterestReference: number): IGroup[] {
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() {
let amount = project.totalAmount;
const projectsSameYear = environment.projectsByStartYear[project.startYear];
for (let i = 0; i < projectsSameYear.length; ++i) {
const otherProject = projectsSameYear[i];
if (otherProject === project) {
break;
}
amount += otherProject.totalAmount;
}
return amount;
}
function median(values: number[]) {
const half = Math.floor(values.length / 2);
if (values.length % 2) {
return values[half];
}
return (values[half - 1] + values[half]) / 2.0;
}
const requiredAmount = calculateRequiredAmount();
const simulatedValuesThisYear = environment.simulatedValues[project.startYear];
simulatedValuesThisYear.sort((a, b) => a - b);
const groups = createGroups(requiredAmount, environment.noInterestReferenceLine[project.startYear]);
const valuesByGroup: { [groupName: string]: number } = {};
const bucketSize = Math.round(simulatedValuesThisYear.length / NUMBER_OF_BUCKETS);
const buckets: IBucket[] = [];
for (let i = 0; i < simulatedValuesThisYear.length; i += bucketSize) {
const bucket: IBucket = {
max: Number.MIN_VALUE,
min: Number.MAX_VALUE,
subBuckets: {}
};
for (let j = i; j < i + bucketSize; ++j) {
const value = simulatedValuesThisYear[j];
bucket.min = Math.min(bucket.min, value);
bucket.max = Math.max(bucket.max, value);
const group = groupForValue(simulatedValuesThisYear[j], groups);
valuesByGroup[group.name] = (valuesByGroup[group.name] || 0) + 1;
const 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);
}
const nonEmptyGroups = groups.filter(group => !!valuesByGroup[group.name]);
nonEmptyGroups.forEach(group => group.percentage = valuesByGroup[group.name] / simulatedValuesThisYear.length);
const oneSixth = Math.round(simulatedValuesThisYear.length / 6);
return {
buckets,
groups: nonEmptyGroups,
max: simulatedValuesThisYear[simulatedValuesThisYear.length - 1],
median: median(simulatedValuesThisYear),
min: simulatedValuesThisYear[0],
project,
twoThird: {
max: simulatedValuesThisYear[simulatedValuesThisYear.length - oneSixth],
min: simulatedValuesThisYear[oneSixth]
}
};
}
export function syncMonteCarlo(options?: IMonteCarloSimulationOptions) {
const environment = createMonteCarloEnvironment(initializeOptions(options));
let projects: IProjectResult[] = [];
for (const project of options!.projects!) {
projects.push(calculateProject(project, environment));
}
return projects;
}
export function parallelMonteCarlo(userOptions?: IMonteCarloSimulationOptions) {
const options = initializeOptions(userOptions);
return parallel
.from(options.projects, { minValuesPerTask: 2 })
.inEnvironment(createMonteCarloEnvironment, options)
.map(calculateProject);
}