Skip to content

Commit

Permalink
C++ Monte Carlo simulation calculator on AWS Lambda
Browse files Browse the repository at this point in the history
  • Loading branch information
press0 committed Jul 11, 2023
1 parent fa438eb commit 729094e
Show file tree
Hide file tree
Showing 11 changed files with 366 additions and 7 deletions.
22 changes: 22 additions & 0 deletions .github/workflows/workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,28 @@ jobs:
cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_CXX_CLANG_TIDY=clang-tidy
cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}}
build-demo:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Install Dependencies
run: sudo apt-get update && sudo apt-get install -y clang-tidy libcurl4-openssl-dev

- name: Build and install lambda runtime
run: |
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=~/lambda-install
make
make install
- name: Build and package demo project
run: |
cd examples/demo
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=~/lambda-install
make
make aws-lambda-package-demo

format:
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ tags
TODO
compile_commands.json
.clangd

#ide
.idea

11 changes: 11 additions & 0 deletions examples/demo/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.9)
set(CMAKE_CXX_STANDARD 11)
project(demo LANGUAGES CXX)
find_package(aws-lambda-runtime)
add_executable(${PROJECT_NAME} "main.cpp")
target_link_libraries(${PROJECT_NAME} PRIVATE AWS::aws-lambda-runtime)
target_compile_features(${PROJECT_NAME} PRIVATE "cxx_std_11")
target_compile_options(${PROJECT_NAME} PRIVATE "-Wall" "-Wextra")

# this line creates a target that packages your binary and zips it up
aws_lambda_package_target(${PROJECT_NAME})
20 changes: 20 additions & 0 deletions examples/demo/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#include <aws/lambda-runtime/runtime.h>

using namespace aws::lambda_runtime;

static invocation_response my_handler(invocation_request const& req)
{
if (req.payload.length() > 42) {
return invocation_response::failure("error message here"/*error_message*/,
"error type here" /*error_type*/);
}

return invocation_response::success("json payload here" /*payload*/,
"application/json" /*MIME type*/);
}

int main()
{
run_handler(my_handler);
return 0;
}
14 changes: 14 additions & 0 deletions examples/monte-carlo-calculator/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
cmake_minimum_required(VERSION 3.5)
set(CMAKE_CXX_STANDARD 11)

project(demo LANGUAGES CXX)

find_package(aws-lambda-runtime REQUIRED)
find_package(AWSSDK COMPONENTS core)
find_package(ZLIB)

add_executable(${PROJECT_NAME} "main.cpp")
target_link_libraries(${PROJECT_NAME} PUBLIC AWS::aws-lambda-runtime ${AWSSDK_LINK_LIBRARIES} )

# this line creates a target that packages your binary and zips it up
aws_lambda_package_target(${PROJECT_NAME} )
173 changes: 173 additions & 0 deletions examples/monte-carlo-calculator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# A simple C++ Monte Carlo simulation calculator on AWS Lambda

Calculate the value of a European vanilla call option in C++ using Monte Carlo simulation.
Deploy the calculator to AWS Lambda.
Invoke the calculator with a REST call.
The number of simulations, `num_sims`, can be overriden with a query parameter.
All other parameters are hard coded.



### Prerequisites
1. aws account
2. aws cli
3. CMake (version 3.9 or later)
4. git
5. Make
6. zip
7. libcurl4-openssl-dev libssl-dev uuid-dev zlib1g-dev libpulse-dev libcurlpp-dev libcrypto++-dev


## Build the C++ AWS SDK
Run the following commands to build the C++ AWS SDK
```bash
$ git clone https://github.com/aws/aws-sdk-cpp.git
$ cd aws-sdk-cpp
$ mkdir build
$ cd build
$ cmake .. -DBUILD_ONLY="core" -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DCUSTOM_MEMORY_MANAGEMENT=OFF -DCMAKE_INSTALL_PREFIX=~/lambda-install
$ make
$ make install
```

## Build a custom C++ lambda runtime
Run the following commands to build the C++ lambda custom runtime:
```bash
$ git clone https://github.com/press0/aws-lambda-cpp.git
$ cd aws-lambda-cpp
$ mkdir build
$ cd build
$ cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=~/lambda-install
$ make && make install
```

## Build the C++ lambda function
Run the following commands to build the C++ lambda function

```bash
$ cd ../examples/monte-carlo-calculator/
$ mkdir build
$ cd build
$ cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DCMAKE_PREFIX_PATH=~/lambda-install
$ make aws-lambda-package-demo
```

Verify that a file named `demo.zip` was created.

## Create the AWS IAM resources

```
$ cat ../trust-policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": ["lambda.amazonaws.com"]
},
"Action": "sts:AssumeRole"
}
]
}
```

Create the IAM role:
```bash
$ aws iam create-role --role-name lambda-demo --assume-role-policy-document file://../trust-policy.json
```
Note the role Arn returned from the command: <API-ENDPOINT-ARN>

Attach the following policy to allow Lambda to write logs in CloudWatch:
```bash
$ aws iam attach-role-policy --role-name lambda-demo --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
```

## Create the AWS Lambda function
```bash
$ aws lambda create-function --function-name demo --role <API-ENDPOINT-ARN> --runtime provided --timeout 15 --memory-size 128 --handler demo --zip-file fileb://demo.zip

# response
{
"FunctionName": "demo",
"FunctionArn": "arn:aws:lambda:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"Runtime": "provided",
"Role": "arn:aws:iam::167712817792:role/lambda-demo",
"Handler": "demo",
"CodeSize": 13765485,
"Description": "",
"Timeout": 15,
"MemorySize": 128,
"LastModified": "2023-07-10T20:52:40.434+0000",
"CodeSha256": "mN/9DIPLA4ZpftWVBTHaTdZsO3nXk+AVHjfyNRoznWg=",
"Version": "$LATEST",
"TracingConfig": {
"Mode": "PassThrough"
},
"RevisionId": "683bf451-3fb4-4a5e-bdf1-0d5fa1158c41",
"State": "Pending",
"StateReason": "The function is being created.",
"StateReasonCode": "Creating",
"PackageType": "Zip"
}
```

## Update the function, as needed
```bash
aws lambda update-function-code --function-name demo --zip-file fileb://demo.zip


```

## Test the function with the aws cli
```bash
$ aws lambda invoke --function-name demo --cli-binary-format raw-in-base64-out --payload '{}' out.txt

$ cat out.txt
{
"message":"OK num_sims=100000, Call price=10.423098 "
}

```

## Add a Lambda endpoint
TODO: use the aws cli or the aws console

`<API-ENDPOINT-ARN>`

## Test the function with curl

```bash
curl 'https://<API-ENDPOINT-ARN>.lambda-url.us-west-2.on.aws?num_sims=1000000'
# response
{
"message": "OK num_sims=1000000, Call price=10.459100 "
}
```

## check performance in CloudWatch
![CloudWatch ](image/cloudwatch.png)
one tenth of a second


## Test the function at scale
todo




## Delete the function
```bash
aws lambda delete-function --function-name demo
```

## Delete the role
todo: detach policies first
```bash
aws iam delete-role --role-name lambda-demo
```

## Citations
1. https://www.quantstart.com/articles/European-vanilla-option-pricing-with-C-via-Monte-Carlo-methods/

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
97 changes: 97 additions & 0 deletions examples/monte-carlo-calculator/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#include <aws/lambda-runtime/runtime.h>
#include <iomanip>
#include <sstream>
#include <algorithm> // Needed for the "max" function
#include <cmath>
#include <iostream>
#include <aws/core/utils/json/JsonSerializer.h>
#include <aws/core/utils/memory/stl/SimpleStringStream.h>

using namespace aws::lambda_runtime;
using namespace Aws::Utils::Json;



// A simple implementation of the Box-Muller algorithm, used to generate
// gaussian random numbers - necessary for the Monte Carlo method below
// Note that C++11 actually provides std::normal_distribution<> in
// the <random> library, which can be used instead of this function
double gaussian_box_muller() {
double x = 0.0;
double y = 0.0;
double euclid_sq = 0.0;

// Continue generating two uniform random variables
// until the square of their "euclidean distance"
// is less than unity
do {
x = 2.0 * rand() / static_cast<double>(RAND_MAX)-1;
y = 2.0 * rand() / static_cast<double>(RAND_MAX)-1;
euclid_sq = x*x + y*y;
} while (euclid_sq >= 1.0);

return x*sqrt(-2*log(euclid_sq)/euclid_sq);
}

// Pricing a European vanilla call option with a Monte Carlo method
double monte_carlo_call_price(const int& num_sims, const double& S, const double& K, const double& r, const double& v, const double& T) {
double S_adjust = S * exp(T*(r-0.5*v*v));
double S_cur = 0.0;
double payoff_sum = 0.0;

for (int i=0; i<num_sims; i++) {
double gauss_bm = gaussian_box_muller();
S_cur = S_adjust * exp(sqrt(v*v*T)*gauss_bm);
payoff_sum += std::max(S_cur - K, 0.0);
}

return (payoff_sum / static_cast<double>(num_sims)) * exp(-r*T);
}

static invocation_response my_handler(invocation_request const& request) {

// parameter list
int num_sims = 100000; // Number of simulated asset paths; override with query parameter 'num_sims`
double S = 100.0; // Option price
double K = 100.0; // Strike price
double r = 0.05; // Risk-free rate (5%)
double v = 0.2; // Volatility of the underlying (20%)
double T = 1.0; // One year until expiry

// validate input
if (request.payload.length() > 111111111) {
return invocation_response::failure("error message here"/*error_message*/, "error type here" /*error_type*/);
}

JsonValue json(request.payload);
if (!json.WasParseSuccessful()) {
return invocation_response::failure("Failed to parse input JSON", "InvalidJSON");
}

auto view = json.View();
Aws::SimpleStringStream ss;
ss << "OK ";

if (view.ValueExists("queryStringParameters")) {
auto query_params = view.GetObject("queryStringParameters");
if (query_params.ValueExists("num_sims") && query_params.GetObject("num_sims").IsString()) {
num_sims = stoi(query_params.GetString("num_sims")); //override default
}
}

ss << "num_sims=" << std::to_string(num_sims) << ", ";

double callprice = monte_carlo_call_price(num_sims, S, K, r, v, T);
ss << "Call price=" << std::to_string(callprice) << " ";
std::cout << ss.str() << std::endl;

JsonValue response;
response.WithString("message", ss.str());
return invocation_response::success(response.View().WriteCompact(), "application/json");
}

int main()
{
run_handler(my_handler);
return 0;
}
12 changes: 12 additions & 0 deletions examples/monte-carlo-calculator/trust-policy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": ["lambda.amazonaws.com"]
},
"Action": "sts:AssumeRole"
}
]
}
3 changes: 1 addition & 2 deletions examples/s3/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,7 @@ int main()
config.region = Aws::Environment::GetEnv("AWS_REGION");
config.caFile = "/etc/pki/tls/certs/ca-bundle.crt";

auto credentialsProvider = Aws::MakeShared<Aws::Auth::EnvironmentAWSCredentialsProvider>(TAG);
S3::S3Client client(credentialsProvider, config);
S3::S3Client client(config);
auto handler_fn = [&client](aws::lambda_runtime::invocation_request const& req) {
return my_handler(req, client);
};
Expand Down
Loading

0 comments on commit 729094e

Please sign in to comment.