Node.js Monte Carlo Simulation

TODO: collapasable, copyable, download, etc

Please implement these Excel requirements in NodeJS

To create a Monte Carlo simulation, consider the following four steps:

Step 1: To project one possible price trajectory, use the historical price data of the asset to generate a series of periodic daily returns using the natural logarithm (note that this equation differs from the usual percentage change formula): Periodic Daily Return = ln(days_price/previous_days_price)

Step 2: Next use the AVERAGE, STDEV.P, and VAR.P functions on the entire resulting series to obtain the average daily return, standard deviation, and variance inputs, respectively. The drift is equal to: `Drift = average_daily_return - (variance / 2) where average_daily_return is equal to excel's average function from the periodic daily returns and variance is excel's var.p function from the daily returns series

Alternatively, drift can be set to 0; this choice reflects a certain theoretical orientation, but the difference will not be huge, at least for shorter time frames.

Step 3: Next, obtain a random input:

Random Value = standardDev * NORMSINV((RAND())

where standardDev is produced from Excel's STDEV.P function from the periodic daily returns series

NORMSINV and RAND are excel functions

The equation for the following day's price is: Next day price = today's price * e raised to (Drift + Random Value)

Step 4: To take e to a given power x in Excel, use the EXP function: EXP(x). Repeat this calculation the desired number of times. (Each repetition represents one day.) The result is a simulation of the asset's future price movement.

By generating an arbitrary number of simulations, you can assess the probability that a security's price will follow a given trajectory. To implement the described Monte Carlo simulation in Node.js, we will follow the steps outlined, translating Excel functions and operations into JavaScript code. This involves using mathematical functions available in JavaScript and additional libraries for functionalities not natively supported, such as generating normally distributed random numbers.

First, ensure you have Node.js installed on your system. You might also need to install some packages for numerical operations and generating normal distributions, as JavaScript does not have built-in functions equivalent to Excel's NORMSINV or RAND. A popular choice for statistical and numerical tasks is the mathjs library.

  1. Install Required Packages: Open your terminal and install mathjs:
npm install mathjs
  1. Implementing the Monte Carlo Simulation: Create a new file, e.g., monteCarloSimulation.js, and open it in your favorite code editor.
const math = require('mathjs');

// Function to calculate periodic daily returns
function calculatePeriodicDailyReturns(prices) {
    let returns = [];
    for (let i = 1; i < prices.length; i++) {
        let dailyReturn = Math.log(prices[i] / prices[i - 1]);
        returns.push(dailyReturn);
    }
    return returns;
}

// Function to simulate one trajectory
function simulatePriceTrajectory(prices, daysToSimulate, drift = 0) {
    let dailyReturns = calculatePeriodicDailyReturns(prices);
    let averageDailyReturn = math.mean(dailyReturns);
    let variance = math.variance(dailyReturns, 'uncorrected'); // Equivalent to VAR.P in Excel
    let standardDeviation = math.std(dailyReturns, 'uncorrected'); // Equivalent to STDEV.P

    if (drift === 0) { // Optionally calculate drift
        drift = averageDailyReturn - (variance / 2);
    }

    let simulatedPrices = [prices[prices.length - 1]]; // Start with the last known price

    for (let i = 0; i < daysToSimulate; i++) {
        let randomValue = standardDeviation * math.invNorm(math.random()); // NORMSINV(RAND())
        let nextDayPrice = simulatedPrices[i] * Math.exp(drift + randomValue);
        simulatedPrices.push(nextDayPrice);
    }

    return simulatedPrices;
}

// Example usage
let historicalPrices = [100, 101, 102, 103, 104, 105]; // Example historical prices
let daysToSimulate = 10; // Number of days to simulate

let simulatedTrajectory = simulatePriceTrajectory(historicalPrices, daysToSimulate);
console.log("Simulated Prices:", simulatedTrajectory);

This script starts by defining a function to calculate the periodic daily returns from an array of historical prices. It then defines a function to simulate one price trajectory over a specified number of days, using the Monte Carlo method as described. The simulatePriceTrajectory function calculates the necessary statistical parameters (average daily return, variance, standard deviation) and uses these to generate a simulated price path.

Note: The math.invNorm function is used as an equivalent to Excel's NORMSINV, and math.random generates a random number between 0 and 1, similar to Excel's RAND function. The mathjs library handles these calculations.

Remember, the accuracy and reliability of the Monte Carlo simulation depend on the quality and relevance of the historical price data used, as well as the assumptions made during the simulation process, such as the calculation of drift and the distribution of returns.math.invNorm is not a functionYou're right, and I apologize for the confusion. The mathjs library does not have a math.invNorm function directly available for the inverse of the normal distribution (which corresponds to Excel's NORMSINV). For generating normally distributed random numbers (which is what we need when we're trying to apply the NORMSINV(RAND()) part of the formula), we can use a different approach.

A common method to generate normally distributed numbers in JavaScript is to use the Box-Muller transform. The Box-Muller transform generates standard normally distributed random numbers from uniformly distributed random numbers.

Let's adjust the implementation to include a function for generating a normally distributed random number using the Box-Muller transform:

const math = require('mathjs');

// Function to calculate periodic daily returns
function calculatePeriodicDailyReturns(prices) {
    let returns = [];
    for (let i = 1; i < prices.length; i++) {
        let dailyReturn = Math.log(prices[i] / prices[i - 1]);
        returns.push(dailyReturn);
    }
    return returns;
}

// Box-Muller transform to generate normally distributed random numbers
function boxMullerRandom() {
    let u = 0, v = 0;
    while (u === 0) u = Math.random(); //Converting [0,1) to (0,1)
    while (v === 0) v = Math.random();
    return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
}

// Function to simulate one trajectory
function simulatePriceTrajectory(prices, daysToSimulate, drift = 0) {
    let dailyReturns = calculatePeriodicDailyReturns(prices);
    let averageDailyReturn = math.mean(dailyReturns);
    let variance = math.variance(dailyReturns, 'uncorrected'); // Equivalent to VAR.P in Excel
    let standardDeviation = math.std(dailyReturns, 'uncorrected'); // Equivalent to STDEV.P

    if (drift === 0) { // Optionally calculate drift
        drift = averageDailyReturn - (variance / 2);
    }

    let simulatedPrices = [prices[prices.length - 1]]; // Start with the last known price

    for (let i = 0; i < daysToSimulate; i++) {
        let randomValue = standardDeviation * boxMullerRandom(); // Using Box-Muller for NORMSINV(RAND())
        let nextDayPrice = simulatedPrices[i] * Math.exp(drift + randomValue);
        simulatedPrices.push(nextDayPrice);
    }

    return simulatedPrices;
}

// Example usage
let historicalPrices = [100, 101, 102, 103, 104, 105]; // Example historical prices
let daysToSimulate = 10; // Number of days to simulate

let simulatedTrajectory = simulatePriceTrajectory(historicalPrices, daysToSimulate);
console.log("Simulated Prices:", simulatedTrajectory);

In this revised version, the boxMullerRandom function generates a normally distributed random number with a mean of 0 and a standard deviation of 1, which we then scale by the standard deviation of the daily returns. This approach should give you the functionality you need to simulate asset price movements accurately.This is great. Can you port this to swift?Certainly! Translating the Node.js code for a Monte Carlo simulation into Swift involves using Swift's standard library and possibly the Accelerate framework for more complex mathematical operations. However, for this example, we'll stick to the standard library since we're mainly dealing with basic arithmetic and logarithmic functions, alongside generating random numbers.

Swift has built-in functions for generating random numbers and performing exponential and logarithmic calculations, making it well-suited for this task. Here's how you can implement the Monte Carlo simulation in Swift:

import Foundation

// Function to calculate periodic daily returns
func calculatePeriodicDailyReturns(prices: [Double]) -> [Double] {
    var returns: [Double] = []
    for i in 1..<prices.count {
        let dailyReturn = log(prices[i] / prices[i - 1])
        returns.append(dailyReturn)
    }
    return returns
}

// Box-Muller transform to generate normally distributed random numbers
func boxMullerRandom() -> Double {
    let u = Double.random(in: Double.ulpOfOne..<1) // (0,1)
    let v = Double.random(in: Double.ulpOfOne..<1) // (0,1)
    return sqrt(-2.0 * log(u)) * cos(2.0 * Double.pi * v)
}

// Function to simulate one trajectory
func simulatePriceTrajectory(prices: [Double], daysToSimulate: Int, drift: Double? = nil) -> [Double] {
    let dailyReturns = calculatePeriodicDailyReturns(prices: prices)
    let averageDailyReturn = dailyReturns.reduce(0, +) / Double(dailyReturns.count)
    let variance = dailyReturns.map { pow($0 - averageDailyReturn, 2) }.reduce(0, +) / Double(dailyReturns.count)
    let standardDeviation = sqrt(variance)
    
    var calculatedDrift = drift ?? (averageDailyReturn - (variance / 2))
    
    var simulatedPrices: [Double] = [prices.last!]

    for _ in 0..<daysToSimulate {
        let randomValue = standardDeviation * boxMullerRandom() // Using Box-Muller
        let nextDayPrice = simulatedPrices.last! * exp(calculatedDrift + randomValue)
        simulatedPrices.append(nextDayPrice)
    }

    return simulatedPrices
}

// Example usage
let historicalPrices: [Double] = [100, 101, 102, 103, 104, 105]
let daysToSimulate = 10

let simulatedTrajectory = simulatePriceTrajectory(prices: historicalPrices, daysToSimulate: daysToSimulate)
print("Simulated Prices:", simulatedTrajectory)

This Swift code performs the same operations as the Node.js version. It calculates periodic daily returns from an array of historical prices, uses the Box-Muller transform to generate normally distributed random numbers, and simulates a price trajectory over a specified number of days.

Note: Swift's Double.random(in:) function generates a random Double within the specified range, and Double.ulpOfOne is used to avoid generating a 0, which is necessary for the logarithm calculation in the Box-Muller transform. This approach ensures that the generated random numbers are strictly within the (0,1) interval, excluding 0.How can I package this code to be used from a SwiftUI view?To integrate the Monte Carlo simulation code into a SwiftUI application, you can encapsulate the simulation logic into a view model and then use this view model in your SwiftUI view. This approach adheres to the MVVM (Model-View-ViewModel) design pattern, which is a standard for structuring SwiftUI applications.

Step 1: Create the ViewModel

First, create a ViewModel that will contain the logic for running the simulation and storing its results. This view model will also provide a method to trigger the simulation from the UI.

import Foundation

class SimulationViewModel: ObservableObject {
    @Published var simulatedPrices: [Double] = []
    
    func runSimulation(withHistoricalPrices prices: [Double], daysToSimulate: Int) {
        // Assuming the Monte Carlo simulation functions are defined here or in another Swift file
        self.simulatedPrices = simulatePriceTrajectory(prices: prices, daysToSimulate: daysToSimulate)
    }
}

Step 2: Create the SwiftUI View

Next, create a SwiftUI view that uses this ViewModel. This view will have a button to trigger the simulation and a way to display the results, such as a list or a chart.

import SwiftUI

struct SimulationView: View {
    @StateObject private var viewModel = SimulationViewModel()
    
    var body: some View {
        VStack {
            Button("Run Simulation") {
                viewModel.runSimulation(withHistoricalPrices: [100, 101, 102, 103, 104, 105], daysToSimulate: 10)
            }
            
            List(viewModel.simulatedPrices, id: \.self) { price in
                Text("\(price, specifier: "%.2f")")
            }
        }
    }
}

Step 3: Integrate the View into Your App

Finally, ensure your SimulationView is used within your app's main view or wherever you see fit. If you're starting a new SwiftUI app, you can set it directly in the App struct.

import SwiftUI

@main
struct MonteCarloApp: App {
    var body: some Scene {
        WindowGroup {
            SimulationView()
        }
    }
}

Additional Considerations

  • Performance: Depending on the complexity and duration of your simulation, consider running the simulation on a background thread to avoid blocking the UI. Swift's concurrency features (async/await or DispatchQueue) can help with this.
  • Data Visualization: While the example above uses a simple list to display the simulated prices, you might want to visualize the data more effectively. SwiftUI doesn't natively support complex charts out of the box, but you can use third-party libraries or integrate UIKit components for more advanced charting capabilities.
  • Data Input: In a real-world application, you might want to allow users to input their own historical prices or select the number of days for the simulation. Consider adding text fields or other input controls to your SwiftUI view to support this.

This setup provides a basic framework for integrating a Monte Carlo simulation into a SwiftUI application, which you can expand and customize based on your specific requirements.If you think about our original EXCEL requirements, do you need boxMullerRandom or can you use native Apple APIs?For the original requirements that involve generating normally distributed random numbers, as you would with Excel's NORMSINV function combined with RAND(), you can indeed utilize native Apple APIs instead of implementing the Box-Muller transform manually. Swift's GameplayKit framework provides a way to generate Gaussian (normally) distributed random numbers, which can simplify the implementation.

To use GameplayKit for generating normally distributed random numbers, you would first import the framework and then use GKGaussianDistribution. This approach is more straightforward and leverages Apple's optimized algorithms for random number generation.

Here's how you can adjust the Monte Carlo simulation to use GameplayKit for the random number generation part:

Step 1: Import GameplayKit

First, make sure to import GameplayKit at the top of your Swift file where you intend to use the normally distributed random numbers.

import GameplayKit

Step 2: Use GKGaussianDistribution

Replace the boxMullerRandom function or any direct call to generate normally distributed numbers with GKGaussianDistribution. Here's an example of how to adjust the simulatePriceTrajectory function:

import Foundation
import GameplayKit

// Function to simulate one trajectory using GameplayKit for normal distribution
func simulatePriceTrajectory(prices: [Double], daysToSimulate: Int, drift: Double? = nil) -> [Double] {
    let dailyReturns = calculatePeriodicDailyReturns(prices: prices)
    let averageDailyReturn = dailyReturns.reduce(0, +) / Double(dailyReturns.count)
    let variance = dailyReturns.map { pow($0 - averageDailyReturn, 2) }.reduce(0, +) / Double(dailyReturns.count)
    let standardDeviation = sqrt(variance)
    
    var calculatedDrift = drift ?? (averageDailyReturn - (variance / 2))
    
    // Setup the Gaussian distribution with mean 0 and the standard deviation of daily returns
    let randomSource = GKRandomSource()
    let gaussianDistribution = GKGaussianDistribution(randomSource: randomSource, mean: 0, deviation: Float(standardDeviation))
    
    var simulatedPrices: [Double] = [prices.last!]

    for _ in 0..<daysToSimulate {
        let randomValue = Double(gaussianDistribution.nextUniform()) // Generate a normally distributed random number
        let nextDayPrice = simulatedPrices.last! * exp(calculatedDrift + randomValue)
        simulatedPrices.append(nextDayPrice)
    }

    return simulatedPrices
}

Integrating with SwiftUI

After adjusting the simulation logic to use GameplayKit for random number generation, you can integrate this logic into your SwiftUI application in the same way as described in the previous response. The ViewModel and View code would remain largely unchanged, except that now the simulation logic uses a more efficient and possibly more accurate method for generating normally distributed random numbers.

Note

  • GameplayKit is a powerful framework not only for games but also for any application that requires randomization with more control and options than what's available in the standard library.
  • When using GKGaussianDistribution, the nextUniform() method actually generates uniformly distributed numbers. For normally distributed numbers, you should use nextInt() or adjust the method according to the specific API provided by GameplayKit for Gaussian distributions. Ensure you're using the correct method to generate numbers according to the Gaussian distribution.I get NaN for the second and remaining numbersIf you're encountering NaN (Not a Number) results from the second number onward in your simulation, it likely stems from an issue in the calculation that involves operations resulting in undefined or non-numeric values. This can happen due to several reasons in the context of your Monte Carlo simulation. Let's diagnose and correct the potential causes:

1. Incorrect Random Value Generation

First, ensure that the method used to generate normally distributed random values is correct. My previous mention of using nextUniform() for Gaussian distribution was incorrect in the context of generating normally distributed values. For GKGaussianDistribution, you should use nextInt() for integers or convert the distribution to generate floating-point numbers correctly for your use case. However, GKGaussianDistribution does not directly provide a method for floating-point numbers that represent Gaussian random variables, so the focus should be on correctly applying the distribution.

Correcting the Gaussian Random Value Generation

Since GKGaussianDistribution primarily deals with integers, and we used it incorrectly, let's correct the approach for generating normally distributed random values suitable for financial simulations. You might consider using another method or correcting the implementation detail.

2. Review Exponential Calculation

The NaN values could also result from the exponential calculation exp(calculatedDrift + randomValue). If calculatedDrift + randomValue is extremely large or results in an undefined operation, the exponential function could return NaN.

Correct Approach Using GameplayKit

For generating normally distributed values correctly, you should use a continuous distribution. Unfortunately, GameplayKit doesn't offer a direct API for continuous Gaussian distributions that return floating-point numbers in a straightforward manner for this use case. You might revert to manual methods or use another library for this specific task.

Alternative: Manual Gaussian Distribution (Box-Muller Method)

Given the confusion and the need for clarity, let's revert to a manual method for generating Gaussian-distributed values, which ensures you have full control over the floating-point operations:

import Foundation

// Box-Muller transform to generate normally distributed random numbers
func boxMullerRandom(standardDeviation: Double, mean: Double = 0) -> Double {
    let u = Double.random(in: Double.ulpOfOne..<1) // Avoid 0
    let v = Double.random(in: Double.ulpOfOne..<1) // Avoid 0
    let z = sqrt(-2.0 * log(u)) * cos(2.0 * Double.pi * v)
    return z * standardDeviation + mean
}

// Adjust your simulation function to use this for generating the random value

Debugging NaN Results

  • Check for Division by Zero: Ensure that no part of your calculation results in a division by zero.
  • Validate Input Data: Make sure all input data (e.g., prices) are valid numbers and there are no zeroes where they shouldn't be, as logarithm of zero or negative numbers would result in NaN.
  • Log Intermediate Values: Add print statements to log intermediate values (calculatedDrift, randomValue, etc.) to pinpoint where NaN first appears.

Final Note

If after these corrections you still face issues, it might be helpful to isolate the problem by testing each function independently (e.g., the random number generation, the drift calculation, and the exponential growth calculation) to ensure each part works as expected before combining them.

Node.js Monte Carlo Simulation created at Tue Jan 20 1970 18:08:20 GMT+0000 (Coordinated Universal Time)

Related Chats

Node.js Monte Carlo Simulation 1.000

Smoothly Animate Value with Timestamp 0.391

Weighted Sampling in Swift 0.344

Retirement Savings Spreadsheet Model 0.331

Logseq Plugin Development Guide 0.330

Popular NodeJS HTTP Libraries 0.327

Luxon for Human-Friendly Durations 0.304

Metrics Middleware for Next.js 0.289

Winston Logging Configuration 0.285

Node.js POST Request Stream 0.279