From 8787fcb047cf0665a94e1d9011f184a82066deb5 Mon Sep 17 00:00:00 2001 From: Siddharth Date: Fri, 3 Nov 2023 23:48:48 -0400 Subject: [PATCH] fixed bug in reading date. --- README.md | 11 +++ src/core/pde/bsm.rs | 39 +++++++++++ src/core/utils.rs | 6 +- src/equity/blackscholes.rs | 15 +++- src/equity/build_contracts.rs | 20 +----- src/equity/vanila_option.rs | 8 +-- src/equity/vol_surface.rs | 50 +++++++++++++- src/{input => examples/CO}/cmdty_option.json | 0 src/{input => examples/EQ}/equity_option.json | 0 src/{input => examples}/IR/ir1.json | 0 src/examples/build/build_ir_curve.json | 68 +++++++++++++++++++ src/main.rs | 26 +++---- src/utils/RNG.rs | 26 +++---- src/utils/parse_json.rs | 58 ++++++++-------- 14 files changed, 245 insertions(+), 82 deletions(-) create mode 100644 src/core/pde/bsm.rs rename src/{input => examples/CO}/cmdty_option.json (100%) rename src/{input => examples/EQ}/equity_option.json (100%) rename src/{input => examples}/IR/ir1.json (100%) create mode 100644 src/examples/build/build_ir_curve.json diff --git a/README.md b/README.md index e720adf..b9dcb8c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ [![Build and Tests](https://github.com/siddharthqs/RustyQLib/actions/workflows/rust.yml/badge.svg)](https://github.com/siddharthqs/RustyQLib/actions/workflows/rust.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) # RUSTYQLib :Pricing Options with Confidence using JSON RustyQLib is a lightweight yet robust quantitative finance library designed for pricing options. @@ -50,6 +51,7 @@ JSON Output Clarity - [ ] Asian ### Instruments: +#### Equity - [x] Equity Option - [ ] Equity Forward Start Option - [ ] Equity Basket @@ -58,6 +60,15 @@ JSON Output Clarity - [ ] Equity Asian - [ ] Equity Rainbow - [ ] Equity Chooser +#### Interest Rate +- [x] Deposit +- [x] FRA +- [ ] Interest Rate Swap +#### Commodities +- [x] Commodity Option +- [ ] Commodity Forward Start Option +- [ ] Commodity Barrier +- [ ] Commodity Lookback ### Pricing engines: diff --git a/src/core/pde/bsm.rs b/src/core/pde/bsm.rs new file mode 100644 index 0000000..91eb369 --- /dev/null +++ b/src/core/pde/bsm.rs @@ -0,0 +1,39 @@ +/// This is Black Scholes Merton PDE solver using finite difference method +/// $ \frac{\partial V}{\partial t} + \frac{1}{2}\sigma^2 S^2 \frac{\partial^2 V}{\partial S^2} + rS\frac{\partial V}{\partial S} - rV = 0 $ +/// $ V(S,T) = max(S-K,0) $ +/// $ V(0,t) = 0 $ +/// $ V(S,t) \rightarrow S $ as $ S \rightarrow \infty $ +///https://de.wikipedia.org/wiki/Thomas-Algorithmus +// pub fn blackscholes_pde(spot:f64,strike:f64,rate:f64,volatility:f64,time_to_maturity:f64,steps:u64,option_type:OptionType) -> f64{ +// let mut grid = Grid::new(spot,strike,rate,volatility,time_to_maturity,steps,option_type); +// grid.solve(); +// let value = grid.get_value(); +// value +// } +// pub struct Grid { +// spot: f64, +// strike: f64, +// rate: f64, +// dividend: f64, +// //volatility:f64, +// time_to_maturity: f64, +// spot_steps: u64, +// time_steps: u64 +// } +// impl Grid{ +// pub fn payoff(&self,spot:f64) -> f64{ +// let payoff = (spot - self.strike).max(0.0); +// payoff +// } +// pub fn build_grid(&self) -> Vec>{ +// let mut grid:Array2 = Array2::zeros((self.time_steps as usize,self.spot_steps as usize)); +// //let mut grid = vec![vec![0.0;self.spot_steps as usize];self.time_steps as usize]; +// for i in 0..self.spot_steps as usize{ +// grid[0][i] = self.payoff(i as f64); +// } +// grid +// } +// } + + + diff --git a/src/core/utils.rs b/src/core/utils.rs index 1834c95..0b1c9bd 100644 --- a/src/core/utils.rs +++ b/src/core/utils.rs @@ -93,15 +93,15 @@ pub struct ContractOutput { } pub fn dN(x: f64) -> f64 { - // Probability density function of standard normal random variable x. + /// Probability density function of standard normal random variable x. let t = -0.5 * x * x; return t.exp() / (SQRT_2 * PI.sqrt()); } pub fn N(x: f64) -> f64 { - //umulative density function of standard normal random variable x. + ///Cumulative density function of standard normal random variable x. let m = probability::distribution::Gaussian::new(0.0, 1.0); let cdf = m.distribution(x); return cdf; } -//} + diff --git a/src/equity/blackscholes.rs b/src/equity/blackscholes.rs index 8d05ee5..417ac2f 100644 --- a/src/equity/blackscholes.rs +++ b/src/equity/blackscholes.rs @@ -152,13 +152,14 @@ impl EquityOption { } pub fn option_pricing() { println!("Welcome to the Black-Scholes Option pricer."); - println!("(Step 1/7) What is the current price of the underlying asset?"); + println!(" What is the current price of the underlying asset?"); print!(">>"); let mut curr_price = String::new(); io::stdin() .read_line(&mut curr_price) .expect("Failed to read line"); - println!("(Step 2/7) Do you want a call option ('C') or a put option ('P') ?"); + println!(" Do you want a call option ('C') or a put option ('P') ?"); + print!(">>"); let mut side_input = String::new(); io::stdin() .read_line(&mut side_input) @@ -170,28 +171,36 @@ pub fn option_pricing() { _ => panic!("Invalide side argument! Side has to be either 'C' or 'P'."), } println!("Stike price:"); + print!(">>"); let mut strike = String::new(); io::stdin() .read_line(&mut strike) .expect("Failed to read line"); println!("Expected annualized volatility in %:"); println!("E.g.: Enter 50% chance as 0.50 "); + print!(">>"); let mut vol = String::new(); io::stdin() .read_line(&mut vol) .expect("Failed to read line"); println!("Risk-free rate in %:"); + print!(">>"); let mut rf = String::new(); io::stdin().read_line(&mut rf).expect("Failed to read line"); println!(" Maturity date in YYYY-MM-DD format:"); let mut expiry = String::new(); + println!("E.g.: Enter 2020-12-31 for 31st December 2020"); + print!(">>"); io::stdin() .read_line(&mut expiry) .expect("Failed to read line"); - let future_date = NaiveDate::parse_from_str(&expiry, "%Y-%m-%d").expect("Invalid date format"); + println!("{:?}", expiry.trim()); + let _d = expiry.trim(); + let future_date = NaiveDate::parse_from_str(&_d, "%Y-%m-%d").expect("Invalid date format"); println!("Dividend yield on this stock:"); + print!(">>"); let mut div = String::new(); io::stdin() .read_line(&mut div) diff --git a/src/equity/build_contracts.rs b/src/equity/build_contracts.rs index 9bfd172..d8052a6 100644 --- a/src/equity/build_contracts.rs +++ b/src/equity/build_contracts.rs @@ -14,23 +14,7 @@ use crate::core::utils::{Contract,ContractStyle}; use crate::equity::utils::{Engine}; use std::collections::BTreeMap; -pub fn build_eq_contracts_from_json(data: Vec) -> Vec> { - let derivatives:Vec> = data.iter().map(|x| EquityOption::from_json(x.clone())).collect(); +pub fn build_eq_contracts_from_json(mut data: Vec) -> Vec> { + let derivatives:Vec> = data.iter().map(|x| EquityOption::from_json(&x)).collect(); return derivatives; } -pub fn build_volatility_surface(mut contracts:Vec>) -> VolSurface { - - let mut vol_tree:BTreeMap> = BTreeMap::new(); - let spot_date = contracts[0].valuation_date; - let spot_price = contracts[0].underlying_price.value; - for i in 0..contracts.len(){ - let mut contract = contracts[i].as_mut(); - let moneyness = contract.underlying_price.value / contract.strike_price as f64; - let volatility = contract.get_imp_vol(); - let maturity = contract.maturity_date; - vol_tree.entry(maturity).or_insert(Vec::new()).push((moneyness,volatility)); - } - let vol_surface:VolSurface = VolSurface::new(vol_tree, spot_price, spot_date, - DayCountConvention::Act365); - return vol_surface; -} \ No newline at end of file diff --git a/src/equity/vanila_option.rs b/src/equity/vanila_option.rs index 3af49ed..1a6b716 100644 --- a/src/equity/vanila_option.rs +++ b/src/equity/vanila_option.rs @@ -33,7 +33,7 @@ impl Instrument for EquityOption { } } } -/// Equity Option represents a real world equity option contract +/// This struct represents a real world equity option contract #[derive(Debug)] pub struct EquityOption { pub option_type: OptionType, @@ -59,8 +59,8 @@ impl EquityOption{ } } impl EquityOption { - pub fn from_json(data: Contract) -> Box { - let market_data = data.market_data.unwrap(); + pub fn from_json(data: &Contract) -> Box { + let market_data = data.market_data.as_ref().unwrap(); let underlying_quote = Quote::new(market_data.underlying_price); //TODO: Add term structure let date = vec![0.01, 0.02, 0.05, 0.1, 0.5, 1.0, 2.0, 3.0]; @@ -168,7 +168,7 @@ mod tests { style: Some("European".to_string()), rate_data: None }; - let option = EquityOption::from_json(data); + let option = EquityOption::from_json(&data); assert_eq!(option.option_type, OptionType::Call); assert_eq!(option.transection, Transection::Buy); assert_eq!(option.underlying_price.value, 100.0); diff --git a/src/equity/vol_surface.rs b/src/equity/vol_surface.rs index 3c7c51a..0659a4c 100644 --- a/src/equity/vol_surface.rs +++ b/src/equity/vol_surface.rs @@ -2,6 +2,8 @@ use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashMap}; use crate::rates::utils::{DayCountConvention}; use chrono::{NaiveDate}; +use crate::core::trade::OptionType; +use super::vanila_option::{EquityOption}; /// Vol Surface is a collection of volatilities for different maturities and strikes #[derive(Clone,Debug,Serialize,Deserialize)] @@ -28,5 +30,51 @@ impl VolSurface { pub fn get_year_fraction(&self,val_date:NaiveDate,maturity_date:NaiveDate) -> f64 { self.day_count.get_year_fraction(val_date,maturity_date) } + pub fn build_eq_vol(mut contracts:Vec>) -> VolSurface { + let mut vol_tree:BTreeMap> = BTreeMap::new(); + let spot_date = contracts[0].valuation_date; + let spot_price = contracts[0].underlying_price.value; + for i in 0..contracts.len(){ + let mut moneyness=1.0; + let mut contract = contracts[i].as_mut(); + match contract.option_type{ + OptionType::Call => { + moneyness = contract.underlying_price.value / contract.strike_price as f64; -} \ No newline at end of file + }, + OptionType::Put => { + moneyness = contract.strike_price / contract.underlying_price.value as f64; + } + _ => { + panic!("Option type not supported"); + } + } + let volatility = contract.get_imp_vol(); + let maturity = contract.maturity_date; + vol_tree.entry(maturity).or_insert(Vec::new()).push((moneyness,volatility)); + } + let vol_surface:VolSurface = VolSurface::new(vol_tree, spot_price, spot_date, + DayCountConvention::Act365); + return vol_surface; + } + +} + +// #[cfg(test)] +// mod tests{ +// use super::*; +// use crate::core::quotes::Quote; +// use crate::core::utils::{Contract,ContractStyle}; +// use crate::equity::utils::{Engine}; +// use crate::core::trade::OptionType; +// use crate::core::trade::OptionStyle; +// use crate::core::trade::OptionStyle::European; +// +// #[test] +// fn test_build_eq_vol(){ +// // write a unit test for this function +// let mut contract = C +// let mut contracts:Vec> = Vec::new(); +// +// +// } \ No newline at end of file diff --git a/src/input/cmdty_option.json b/src/examples/CO/cmdty_option.json similarity index 100% rename from src/input/cmdty_option.json rename to src/examples/CO/cmdty_option.json diff --git a/src/input/equity_option.json b/src/examples/EQ/equity_option.json similarity index 100% rename from src/input/equity_option.json rename to src/examples/EQ/equity_option.json diff --git a/src/input/IR/ir1.json b/src/examples/IR/ir1.json similarity index 100% rename from src/input/IR/ir1.json rename to src/examples/IR/ir1.json diff --git a/src/examples/build/build_ir_curve.json b/src/examples/build/build_ir_curve.json new file mode 100644 index 0000000..33dbfd9 --- /dev/null +++ b/src/examples/build/build_ir_curve.json @@ -0,0 +1,68 @@ +{"asset":"IR", +"contracts" : [ +{ +"action":"PV", +"pricer":"Analytical", +"asset":"IR", +"rate_data":{ + "instrument": "Deposit", + "currency": "USD", + "start_date": "0M", + "maturity_date":"1M", + "valuation_date": "0M", + "notional": 1000000, + "fix_rate": 0.055, + "day_count": "A360", + "business_day_adjustment": 0 +} +}, +{ +"action":"PV", +"pricer":"Analytical", +"asset":"IR", +"rate_data":{ + "instrument": "Deposit", + "currency": "USD", + "start_date": "0M", + "maturity_date":"3M", + "valuation_date": "0M", + "notional": 1000000, + "fix_rate": 0.05, + "day_count": "A360", + "business_day_adjustment": 0 +} +}, +{ + "action":"PV", + "pricer":"Analytical", + "asset":"IR", + "rate_data":{ + "instrument": "FRA", + "currency": "USD", + "start_date": "3M", + "maturity_date":"6M", + "valuation_date": "0M", + "notional": 1000000, + "fix_rate": 0.06, + "day_count": "A360", + "business_day_adjustment": 0 + } +}, +{ + "action":"PV", + "pricer":"Analytical", + "asset":"IR", + "rate_data":{ + "instrument": "FRA", + "currency": "USD", + "start_date": "6M", + "maturity_date":"9M", + "valuation_date": "0M", + "notional": 1000000, + "fix_rate": 0.065, + "day_count": "A360", + "business_day_adjustment": 0 + } +} +] +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 7488746..aa4117d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,11 +3,11 @@ // extern crate rand_pcg; use rand; -use rand::{SeedableRng}; -use chrono::{Local,DateTime,NaiveDate,NaiveTime,Datelike, Duration}; -use rand::distributions::{Standard,Uniform}; -use rand::distributions::Distribution; -use rand_distr::StandardNormal; +//use rand::{SeedableRng}; +//use chrono::{Local,DateTime,NaiveDate,NaiveTime,Datelike, Duration}; +//use rand::distributions::{Standard,Uniform}; +//use rand::distributions::Distribution; +//use rand_distr::StandardNormal; mod equity; mod core; mod utils; @@ -25,17 +25,17 @@ use std::io::Read; use std::{io, thread}; use std::collections::HashMap; use std::error::Error; -use csv; +//use csv; //use std::env::{args,Args}; -use utils::read_csv; -use utils::RNG; +//use utils::read_csv; +//use utils::RNG; -use std::env::{args, temp_dir}; -use rand::Rng; +//use std::env::{args, temp_dir}; +//use rand::Rng; use equity::blackscholes; use crate::equity::montecarlo; use clap::{App, Arg, ArgMatches, SubCommand}; -use std::env; +//use std::env; use utils::parse_json; use std::time::{Instant}; @@ -146,7 +146,7 @@ fn main() { let input_dir = dir_matches.value_of("input").unwrap(); let output_dir = dir_matches.value_of("output").unwrap(); let start_time = Instant::now(); - let mut output_vec:Vec = Vec::new(); + let output_vec:Vec = Vec::new(); let files = fs::read_dir(input_dir).unwrap(); for ifile in files { let ifile = ifile.unwrap(); @@ -167,7 +167,7 @@ fn main() { println!("Time taken to process the dir: {:?}", elapsed_time); } ("interactive",Some(interactive_matches)) => { - println!("Welcome to Option pricing CLI"); + println!("Welcome to Option pricing CLI"); loop { println!(" Do you want to price option (1) or calculate implied volatility (2)? or (3) to exit"); diff --git a/src/utils/RNG.rs b/src/utils/RNG.rs index ba7c186..460c16b 100644 --- a/src/utils/RNG.rs +++ b/src/utils/RNG.rs @@ -14,26 +14,26 @@ use std::io::prelude::*; use std::fs; use std::path::Path; use std::env::temp_dir; +/// Random Number Generator using marsaglia polar method fn generate_standard_normal_marsaglia_polar() -> (f64, f64) { let mut rng = rand::thread_rng(); - let mut X = 0.0; - let mut Y = 0.0; - let mut S = 0.0f64; + let mut x = 0.0; + let mut y = 0.0; + let mut s = 0.0f64; - while(true) { - X = Uniform::new(0.0,1.0).sample(&mut rng)*2.0 -1.0; - Y = Uniform::new(0.0,1.0).sample(&mut rng)*2.0 -1.0; - S = X*X + Y*Y; - if S<1.0f64 && S != 0.0f64 { + while true { + x = Uniform::new(0.0,1.0).sample(&mut rng)*2.0 -1.0; + y = Uniform::new(0.0,1.0).sample(&mut rng)*2.0 -1.0; + s = x*x + y*y; + if s<1.0f64 && s != 0.0f64 { break; } } - - let I = ((-2.0 * S.ln()) / S).sqrt(); - (I*X,I*Y) + let i = ((-2.0 * s.ln()) / s).sqrt(); + (i*x,i*y) } - +/// Random Number Generator using Box Muller method fn generate_standard_normal_box() -> (f64, f64) { let mut rng = rand::thread_rng(); @@ -122,6 +122,7 @@ pub fn get_matrix_standard_normal(size_n:u64,size_m:u64)-> Vec> { } rn_vec_n } +/// Write a vector of f64 to a file using BigEndian byte order fn write_to_file_byteorder>(data: &[f64], path: P) -> std::io::Result<()> { let mut file = File::create(path)?; for f in data { @@ -129,6 +130,7 @@ fn write_to_file_byteorder>(data: &[f64], path: P) -> std::io::Re } Ok(()) } +/// Read a vector of f64 from a file using BigEndian byte order fn read_from_file_byteorder>(path: P) -> std::io::Result> { let mut file = File::open(path)?; let buf_len = file.metadata()?.len() / 8; // 8 bytes for one f64 diff --git a/src/utils/parse_json.rs b/src/utils/parse_json.rs index 2abb6b2..cbe2e73 100644 --- a/src/utils/parse_json.rs +++ b/src/utils/parse_json.rs @@ -24,7 +24,26 @@ use std::env::temp_dir; use crate::rates; use crate::rates::deposits::Deposit; use crate::rates::build_contracts::{build_ir_contracts, build_ir_contracts_from_json, build_term_structure}; -use crate::equity::build_contracts::{build_volatility_surface, build_eq_contracts_from_json}; +use crate::equity::build_contracts::{build_eq_contracts_from_json}; +use crate::equity::vol_surface::VolSurface; + +/// This function saves the output to a file and returns the path to the file. +pub fn save_to_file<'a>(output_folder: &'a str, subfolder: &'a str, filename: &'a str, output: &'a str) -> String { + let mut dir = std::path::PathBuf::from(output_folder); + if subfolder.len() > 0 { + dir.push(subfolder); + } + let _dir = dir.as_path(); + if !_dir.exists() { + let _ = fs::create_dir(_dir); + } + dir.push(filename); + let mut file = File::create(&dir).expect("Failed to create file"); + file.write_all(output.as_bytes()).expect("Failed to write to file"); + return dir.as_path().to_str().unwrap().to_string(); +} + +/// This function different types of curves such as term structure, volatility surface, etc. pub fn build_curve(mut file: &mut File,output_filename: &str)->() { let mut contents = String::new(); file.read_to_string(&mut contents) @@ -34,51 +53,34 @@ pub fn build_curve(mut file: &mut File,output_filename: &str)->() { panic!("No contracts found in JSON file"); } else if list_contracts.asset=="EQ"{ + println!("Building volatility surface"); let mut contracts:Vec> = build_eq_contracts_from_json(list_contracts.contracts); - let vol_surface = build_volatility_surface(contracts); - let mut dir = std::path::PathBuf::from(output_filename); - - dir.push("vol_surface"); - let vol_dir = dir.as_path(); - if !vol_dir.exists() { - let _ = fs::create_dir(vol_dir); - } - dir.push("vol_surface.json"); - //Todo write Vol Surface to file - - let mut file = File::create(dir).expect("Failed to create file"); - let mut output: String = String::new(); + let vol_surface = VolSurface::build_eq_vol(contracts); let serialized_vol_surface = serde_json::to_string(&vol_surface).unwrap(); - file.write_all(serialized_vol_surface.as_bytes()).expect("Failed to write to file"); - + let out_dir = save_to_file(output_filename, "vol_surface", "vol_surface.json", &serialized_vol_surface); + println!("Volatility surface saved to {}", out_dir); } else if list_contracts.asset=="CO"{ + //Todo -build commodity vol surface panic!("Commodity contracts not supported"); } else if list_contracts.asset=="IR"{ let mut contracts:Vec> = build_ir_contracts_from_json(list_contracts.contracts); let ts = build_term_structure(contracts); - let mut dir = std::path::PathBuf::from(output_filename); - - dir.push("term_structure"); - let ts_dir = dir.as_path(); - if !ts_dir.exists() { - let _ = fs::create_dir(ts_dir); - } - dir.push("term_structure.csv"); - let mut file = File::create(dir).expect("Failed to create file"); let mut output: String = String::new(); for i in 0..ts.date.len(){ output.push_str(&format!("{},{},{}\n",ts.date[i],ts.discount_factor[i],ts.rate[i])); } - file.write_all(output.as_bytes()).expect("Failed to write to file"); + + let out_dir = save_to_file(output_filename, "term_structure", "term_structure.csv", &output); + println!("Term structure saved to {}", out_dir); + } else{ panic!("Asset class not supported"); } } - pub fn parse_contract(mut file: &mut File,output_filename: &str) { let mut contents = String::new(); file.read_to_string(&mut contents) @@ -108,7 +110,7 @@ pub fn process_contract(data: utils::Contract) -> String { if data.action=="PV" && data.asset=="EQ"{ //let market_data = data.market_data.clone().unwrap(); - let option = EquityOption::from_json(data.clone()); + let option = EquityOption::from_json(&data); let contract_output = utils::ContractOutput{pv:option.npv(),delta:option.delta(),gamma:option.gamma(),vega:option.vega(),theta:option.theta(),rho:option.rho(), error: None }; println!("Theoretical Price ${}", contract_output.pv);