diff --git a/examples/images/collar.png b/examples/images/collar.png new file mode 100644 index 0000000..5e5ea51 Binary files /dev/null and b/examples/images/collar.png differ diff --git a/examples/images/fence.png b/examples/images/fence.png new file mode 100644 index 0000000..6b07f5e Binary files /dev/null and b/examples/images/fence.png differ diff --git a/examples/images/ladder_strategy.png b/examples/images/ladder_strategy.png new file mode 100644 index 0000000..fbbd16c Binary files /dev/null and b/examples/images/ladder_strategy.png differ diff --git a/examples/images/risk_reversal.png b/examples/images/risk_reversal.png new file mode 100644 index 0000000..f0d91e1 Binary files /dev/null and b/examples/images/risk_reversal.png differ diff --git a/examples/options_pricing.rs b/examples/options_pricing.rs index 514b17b..83d08f9 100644 --- a/examples/options_pricing.rs +++ b/examples/options_pricing.rs @@ -273,14 +273,36 @@ fn example_strategy() { model.price(&put) ); + let otm_put = EuropeanOption::new(instrument.clone(), 40.0, 1.0, Put); + let otm_call = EuropeanOption::new(instrument.clone(), 60.0, 1.0, Call); + println!( + "[Collar: {:?}], given stock: {}, put: {}, call: {}", + model.collar(&instrument, &otm_put, &otm_call)(50.0), + instrument.spot, + model.price(&otm_put), + model.price(&otm_call) + ); + + let atm_put = EuropeanOption::new(instrument.clone(), 50.0, 1.0, Put); + let otm_call = EuropeanOption::new(instrument.clone(), 60.0, 1.0, Call); + let otm_put = EuropeanOption::new(instrument.clone(), 40.0, 1.0, Put); + println!( + "[Fence: {:?}], given stock: {}, put: {}, put: {}, call: {}", + model.fence(&instrument, &atm_put, &otm_put, &otm_call)(50.0), + instrument.spot, + model.price(&atm_put), + model.price(&otm_call), + model.price(&otm_put) + ); + // [Covered Call: 50.46060396445954], given stock: 50, call: 0.4606039644595379 // [Protective Put: 50.19404262184266], given stock: 50, put: 0.19404262184266008 //////////// /* SIMPLE */ - let itm_call = EuropeanOption::new(instrument.clone(), 40.0, 1.0, Call); let itm_put = EuropeanOption::new(instrument.clone(), 60.0, 1.0, Put); + let itm_call = EuropeanOption::new(instrument.clone(), 40.0, 1.0, Call); println!( "[Guts: {:?}], given put: {}, call: {}", model.guts(&itm_put, &itm_call)(50.0), @@ -288,8 +310,8 @@ fn example_strategy() { model.price(&itm_call) ); - let atm_call = EuropeanOption::new(instrument.clone(), 50.0, 1.0, Call); let atm_put = EuropeanOption::new(instrument.clone(), 50.0, 1.0, Put); + let atm_call = EuropeanOption::new(instrument.clone(), 50.0, 1.0, Call); println!( "[Straddle: {:?}], given put: {}, call: {}", model.straddle(&atm_put, &atm_call)(50.0), @@ -297,8 +319,8 @@ fn example_strategy() { model.price(&atm_call) ); - let otm_call = EuropeanOption::new(instrument.clone(), 60.0, 1.0, Call); let otm_put = EuropeanOption::new(instrument.clone(), 40.0, 1.0, Put); + let otm_call = EuropeanOption::new(instrument.clone(), 60.0, 1.0, Call); println!( "[Strangle: {:?}], given put: {}, call: {}", model.strangle(&otm_put, &otm_call)(50.0), @@ -306,9 +328,14 @@ fn example_strategy() { model.price(&otm_call) ); - // [Guts: 20.604709034251407], given put: 10.310791308307145, call: 10.293917725944262 - // [Straddle: 5.971892724319904], given put: 2.923524422096456, call: 3.048368302223448 - // [Strangle: 0.654646586302198], given put: 0.19404262184266008, call: 0.4606039644595379 + let otm_put = EuropeanOption::new(instrument.clone(), 40.0, 1.0, Put); + let otm_call = EuropeanOption::new(instrument.clone(), 60.0, 1.0, Call); + println!( + "[Risk Reversal: {:?}], given put: {}, call: {}", + model.risk_reversal(&otm_put, &otm_call)(50.0), + model.price(&otm_put), + model.price(&otm_call) + ); /////////////// /* BUTTERFLY */ @@ -361,13 +388,24 @@ fn example_strategy() { let long1 = EuropeanOption::new(instrument.clone(), 55.0, 1.0, Call); let long2 = EuropeanOption::new(instrument.clone(), 55.0, 1.0, Call); println!( - "[Back Spread: {:?}], given short: {}, long1: {}, given long2: {}", + "[Back Spread: {:?}], given short: {}, long1: {}, long2: {}", model.back_spread(&short, &long1, &long2)(50.0), model.price(&short), model.price(&long1), model.price(&long2), ); + let long = EuropeanOption::new(instrument.clone(), 50.0, 1.0, Call); + let short1 = EuropeanOption::new(instrument.clone(), 55.0, 1.0, Call); + let short2 = EuropeanOption::new(instrument.clone(), 55.0, 1.0, Call); + println!( + "[Ladder: {:?}], given long: {}, short1: {}, short2: {}", + model.ladder(&long, &short1, &short2)(50.0), + model.price(&short), + model.price(&long1), + model.price(&long2), + ); + let front_month = EuropeanOption::new(instrument.clone(), 50.0, 1.0 / 12.0, Call); let back_month = EuropeanOption::new(instrument.clone(), 50.0, 2.0 / 12.0, Call); println!( @@ -441,6 +479,33 @@ fn example_strategy() { ); // => Protective Put: examples/images/protective_put.png + let options = vec![ + EuropeanOption::new(instrument.clone(), 40.0, 1.0, Put), + EuropeanOption::new(instrument.clone(), 60.0, 1.0, Call), + ]; + let _ = model.plot_strategy_breakdown( + "Collar", + model.collar(&instrument, &options[0], &options[1]), + 20.0..80.0, + "examples/images/collar.png", + &options, + ); + // => Protective Put: examples/images/collar.png + + let options = vec![ + EuropeanOption::new(instrument.clone(), 50.0, 1.0, Put), + EuropeanOption::new(instrument.clone(), 40.0, 1.0, Put), + EuropeanOption::new(instrument.clone(), 60.0, 1.0, Call), + ]; + let _ = model.plot_strategy_breakdown( + "Fence", + model.fence(&instrument, &options[0], &options[1], &options[2]), + 20.0..80.0, + "examples/images/fence.png", + &options, + ); + // => Protective Put: examples/images/fence.png + let options = vec![ EuropeanOption::new(instrument.clone(), 60.0, 1.0, Put), EuropeanOption::new(instrument.clone(), 40.0, 1.0, Call), @@ -481,6 +546,19 @@ fn example_strategy() { ); // => Strangle: examples/images/strangle_strategy.png + let options = vec![ + EuropeanOption::new(instrument.clone(), 40.0, 1.0, Put), + EuropeanOption::new(instrument.clone(), 60.0, 1.0, Call), + ]; + let _ = model.plot_strategy_breakdown( + "Risk Reversal", + model.risk_reversal(&options[0], &options[1]), + 20.0..80.0, + "examples/images/risk_reversal.png", + &options, + ); + // => Strangle: examples/images/risk_reversal.png + let options = vec![ EuropeanOption::new(instrument.clone(), 40.0, 1.0, Call), EuropeanOption::new(instrument.clone(), 50.0, 1.0, Call), @@ -576,6 +654,19 @@ fn example_strategy() { &options, ); // => Back Spread: examples/images/back_spread_strategy.png + let options = vec![ + EuropeanOption::new(instrument.clone(), 50.0, 1.0, Call), + EuropeanOption::new(instrument.clone(), 55.0, 1.0, Call), + EuropeanOption::new(instrument.clone(), 55.0, 1.0, Call), + ]; + let _ = model.plot_strategy_breakdown( + "Ladder", + model.ladder(&options[0], &options[1], &options[2]), + 20.0..80.0, + "examples/images/ladder_strategy.png", + &options, + ); + let options = vec![ EuropeanOption::new(instrument.clone(), 50.0, 1.0 / 12.0, Call), EuropeanOption::new(instrument.clone(), 50.0, 2.0 / 12.0, Call), diff --git a/src/options/traits/option_strategy.rs b/src/options/traits/option_strategy.rs index de307be..746e95b 100644 --- a/src/options/traits/option_strategy.rs +++ b/src/options/traits/option_strategy.rs @@ -424,7 +424,7 @@ pub trait OptionStrategy: OptionPricing { check_is_call!(call); assert!( stock.spot > 0.0 && call.otm(), - "Stock must be ITM and call must be OTM!" + "Stock price must be positive and call must be OTM!" ); let price = stock.spot - self.price(call); @@ -443,7 +443,7 @@ pub trait OptionStrategy: OptionPricing { check_is_put!(put); assert!( stock.spot > 0.0 && put.otm(), - "Stock must be ITM and put must be OTM!" + "Stock price must be positive and put must be OTM!" ); let price = stock.spot + self.price(put); @@ -452,6 +452,61 @@ pub trait OptionStrategy: OptionPricing { } } + /// The collar strategy involves owning the underlying, buying a protective put and selling a covered call with the same expiration date. + fn collar<'a, T: Option>( + &'a self, + stock: &'a Instrument, + otm_put: &'a T, + otm_call: &'a T, + ) -> impl Fn(f64) -> (f64, f64) + 'a { + move |spot_price| { + check_same_expiration_date!(otm_put, otm_call); + check_is_put!(otm_put); + check_is_call!(otm_call); + + assert!( + stock.spot > 0.0 && otm_put.otm() && otm_call.otm(), + "Stock price must be positive and options must be OTM!" + ); + + let price = stock.spot + self.price(otm_put) - self.price(otm_call); + let payoff = + spot_price + otm_put.payoff(Some(spot_price)) - otm_call.payoff(Some(spot_price)); + (payoff, price) + } + } + + /// The fence strategy consists of a long position in a financial instrument, a long ATM put and short positions in a OTM call and a OTM put. + fn fence<'a, T: Option>( + &'a self, + stock: &'a Instrument, + atm_put: &'a T, + otm_put: &'a T, + otm_call: &'a T, + ) -> impl Fn(f64) -> (f64, f64) + 'a { + move |spot_price| { + check_same_expiration_date!(atm_put, otm_put); + check_same_expiration_date!(otm_put, otm_call); + check_is_put!(atm_put); + check_is_put!(otm_put); + check_is_call!(otm_call); + + assert!( + stock.spot > 0.0 && otm_put.otm() && otm_call.otm() && atm_put.atm(), + "Stock price must be positive and options must be OTM and ATM!" + ); + + let price = stock.spot + self.price(otm_put) + self.price(atm_put) + - self.price(otm_put) + - self.price(otm_call); + let payoff = + spot_price + otm_put.payoff(Some(spot_price)) + atm_put.payoff(Some(spot_price)) + - otm_put.payoff(Some(spot_price)) + - otm_call.payoff(Some(spot_price)); + (payoff, price) + } + } + /* SIMPLE */ /// Buy (long gut) or sell (short gut) a pair of ITM (in the money) put and call. @@ -505,13 +560,33 @@ pub trait OptionStrategy: OptionPricing { check_is_call!(call); check_is_put!(put); - // assert!(put.otm() && call.otm(), "Put and call must be OTM!"); + assert!(put.otm() && call.otm(), "Put and call must be OTM!"); + let price = self.price(put) + self.price(call); let payoff = put.payoff(Some(spot_price)) + call.payoff(Some(spot_price)); (payoff, price) } } + /// A risk-reversal is an option position that consists of shorting an OTM put and being long in an OTM call expiring on the same expiration date. + fn risk_reversal<'a, T: Option>( + &'a self, + put: &'a T, + call: &'a T, + ) -> impl Fn(f64) -> (f64, f64) + 'a { + move |spot_price| { + check_same_expiration_date!(put, call); + check_is_call!(call); + check_is_put!(put); + + assert!(put.otm() && call.otm(), "Put and call must be OTM!"); + + let price = self.price(call) - self.price(put); + let payoff = call.payoff(Some(spot_price)) - put.payoff(Some(spot_price)); + (payoff, price) + } + } + /* BUTTERFLY */ /// Long butterfly spreads use four option contracts with the same expiration but three different strike prices to create a range of prices the strategy can profit from. @@ -771,6 +846,41 @@ pub trait OptionStrategy: OptionPricing { } } + /// The ladder strategy, also known as a Christmas tree, is a combination of three options of the same type (all calls or all puts) at three different strike prices. + fn ladder<'a, T: Option>( + &'a self, + long: &'a T, + short1: &'a T, + short2: &'a T, + ) -> impl Fn(f64) -> (f64, f64) + 'a { + move |spot_price| { + check_same_expiration_date!(long, short1); + check_same_expiration_date!(short1, short2); + + if long.is_call() { + check_is_call!(short1); + check_is_call!(short2); + assert!( + long.atm() && short1.otm() && short2.otm(), + "Long call must be ATM and short calls must be OTM!" + ); + } else { + check_is_put!(short1); + check_is_put!(short2); + assert!( + long.atm() && short1.otm() && short2.otm(), + "Long put must be ATM and short puts must be OTM!" + ); + } + + let price = self.price(long) - self.price(short1) - self.price(short2); + let payoff = long.payoff(Some(spot_price)) + - short1.payoff(Some(spot_price)) + - short2.payoff(Some(spot_price)); + (payoff, price) + } + } + /// Short an ATM (at the money) call/put near-term expiration ("front-month") and long an ATM call/put with expiration one month later ("back-month"). /// Used when a trader expects a gradual or sideways movement in the short term and has more direction bias over the life of the longer-dated option. fn calendar_spread<'a, T: Option>( @@ -871,61 +981,14 @@ pub trait OptionStrategy: OptionPricing { } } - /* SPREAD */ - - /// TODO - /// The collar strategy involves buying a protective put and selling a covered call with the same expiration date. - fn collar(&self, option: &T) -> f64 { - panic!("Collar not implemented for this model"); - } - - /// TODO - /// The fence strategy involves buying a call and selling a put with the same expiration date, but different strike prices. - fn fence(&self, option: &T) -> f64 { - panic!("Fence not implemented for this model"); - } - - /// TODO - /// The jelly roll strategy involves buying a call and put with the same expiration date, but different strike prices, and selling a call and put with different strike prices. - fn jelly_roll(&self, option: &T) -> f64 { - panic!("Jelly roll not implemented for this model"); - } - - /// TODO - /// The strap strategy involves buying two calls and one put with the same expiration date and strike price. - fn strap(&self, option: &T) -> f64 { - panic!("Strap not implemented for this model"); - } - - /// TODO - /// The strip strategy involves buying two puts and one call with the same expiration date and strike price. - fn strip(&self, option: &T) -> f64 { - panic!("Strip not implemented for this model"); - } - - /// TODO - /// The christmas tree strategy involves buying one call and two puts with the same expiration date and strike price. - fn christmas_tree(&self, option: &T) -> f64 { - panic!("Christmas tree not implemented for this model"); - } - /* ALIASES */ - - /// TODO - fn ladder(&self, option: &T) -> f64 { + fn christmas_tree<'a, T: Option>( + &'a self, + long: &'a T, + short1: &'a T, + short2: &'a T, + ) -> impl Fn(f64) -> (f64, f64) { log_info!("Ladder strategy is equivalent to the Christmas Tree strategy!"); - self.christmas_tree(option) - } - - /// TODO - fn risk_reversal(&self, option: &T) -> f64 { - log_info!("Risk reversal strategy is equivalent to the Butterfly strategy!"); - todo!() - } - - /// TODO - fn synthetic_long(&self, option: &T) -> f64 { - log_info!("Synthetic long strategy is equivalent to the Long Call strategy!"); - todo!() + self.ladder(long, short1, short2) } } diff --git a/tests/options_pricing.rs b/tests/options_pricing.rs index 39f8fdf..e9f24a2 100644 --- a/tests/options_pricing.rs +++ b/tests/options_pricing.rs @@ -1685,6 +1685,21 @@ mod test_option_strategies { (50.0, 50.19404262184266), ); + let otm_call = EuropeanOption::new(instrument.clone(), 60.0, 1.0, OptionType::Call); + let otm_put = EuropeanOption::new(instrument.clone(), 40.0, 1.0, OptionType::Put); + assert_eq!( + model.collar(&instrument, &otm_put, &otm_call)(50.0), + (50.0, 49.733438657383125), + ); + + let atm_put = EuropeanOption::new(instrument.clone(), 50.0, 1.0, OptionType::Put); + let otm_call = EuropeanOption::new(instrument.clone(), 60.0, 1.0, OptionType::Call); + let otm_put = EuropeanOption::new(instrument.clone(), 40.0, 1.0, OptionType::Put); + assert_eq!( + model.fence(&instrument, &atm_put, &otm_put, &otm_call)(50.0), + (50.0, 52.46292045763692), + ); + // Simple Strategies let itm_call = EuropeanOption::new(instrument.clone(), 40.0, 1.0, OptionType::Call); let itm_put = EuropeanOption::new(instrument.clone(), 60.0, 1.0, OptionType::Put); @@ -1707,6 +1722,13 @@ mod test_option_strategies { (0.0, 0.654646586302198), ); + let otm_call = EuropeanOption::new(instrument.clone(), 60.0, 1.0, OptionType::Call); + let otm_put = EuropeanOption::new(instrument.clone(), 40.0, 1.0, OptionType::Put); + assert_eq!( + model.risk_reversal(&otm_put, &otm_call)(50.0), + (0.0, 0.2665613426168778), + ); + // Butterfly Strategies let lower = EuropeanOption::new(instrument.clone(), 40.0, 1.0, OptionType::Call); let body = EuropeanOption::new(instrument.clone(), 50.0, 1.0, OptionType::Call); @@ -1774,6 +1796,24 @@ mod test_option_strategies { (0.0, -0.4818434154443594), ); + let long = EuropeanOption::new(instrument.clone(), 50.0, 1.0, OptionType::Call); + let short1: EuropeanOption = + EuropeanOption::new(instrument.clone(), 55.0, 1.0, OptionType::Call); + let short2 = EuropeanOption::new(instrument.clone(), 55.0, 1.0, OptionType::Call); + assert_eq!( + model.ladder(&long, &short1, &short2)(50.0), + (0.0, 0.4818434154443594), + ); + + let long = EuropeanOption::new(instrument.clone(), 50.0, 1.0, OptionType::Call); + let short1: EuropeanOption = + EuropeanOption::new(instrument.clone(), 55.0, 1.0, OptionType::Call); + let short2 = EuropeanOption::new(instrument.clone(), 55.0, 1.0, OptionType::Call); + assert_eq!( + model.christmas_tree(&long, &short1, &short2)(50.0), + (0.0, 0.4818434154443594), + ); + let front_month = EuropeanOption::new(instrument.clone(), 50.0, 1.0 / 12.0, OptionType::Call); let back_month = @@ -1839,6 +1879,15 @@ mod test_option_strategies { (0.0, -0.9607806830432359), ); + let long = EuropeanOption::new(instrument.clone(), 50.0, 1.0, OptionType::Put); + let short1: EuropeanOption = + EuropeanOption::new(instrument.clone(), 45.0, 1.0, OptionType::Put); + let short2 = EuropeanOption::new(instrument.clone(), 45.0, 1.0, OptionType::Put); + assert_eq!( + model.ladder(&long, &short1, &short2)(50.0), + (0.0, 0.9607806830432359), + ); + let front_month = EuropeanOption::new(instrument.clone(), 50.0, 1.0 / 12.0, OptionType::Put); let back_month = EuropeanOption::new(instrument.clone(), 50.0, 2.0 / 12.0, OptionType::Put);