use crate::chains::OptionData;
use crate::constants::{STRIKE_PRICE_LOWER_BOUND_MULTIPLIER, STRIKE_PRICE_UPPER_BOUND_MULTIPLIER};
use crate::error::strategies::BreakEvenErrorKind;
use crate::{
ExpirationDate, Options,
chains::{StrategyLegs, chain::OptionChain, utils::OptionDataGroup},
error::{OperationErrorKind, position::PositionError, strategies::StrategyError},
greeks::Greeks,
model::{
Trade,
position::Position,
types::{Action, OptionBasicType, OptionStyle, OptionType, Side},
},
pnl::PnLCalculator,
pricing::payoff::Profit,
strategies::{
StrategyConstructor,
delta_neutral::DeltaNeutrality,
probabilities::core::ProbabilityAnalysis,
utils::{FindOptimalSide, OptimizationCriteria, calculate_price_range},
},
visualization::Graph,
};
use positive::Positive;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::str::FromStr;
use tracing::error;
use utoipa::ToSchema;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema)]
pub struct StrategyBasics {
pub name: String,
pub kind: StrategyType,
pub description: String,
}
pub trait Strategable:
Strategies
+ StrategyConstructor
+ Profit
+ Graph
+ ProbabilityAnalysis
+ Greeks
+ DeltaNeutrality
+ PnLCalculator
{
fn info(&self) -> Result<StrategyBasics, StrategyError> {
Err(StrategyError::operation_not_supported(
"info",
std::any::type_name::<Self>(),
))
}
fn type_name(&self) -> StrategyType {
match self.info() {
Ok(info) => info.kind,
Err(_) => {
panic!("Invalid strategy type");
}
}
}
fn name(&self) -> String {
match self.info() {
Ok(info) => info.name,
Err(_) => {
panic!("Invalid strategy name");
}
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)]
pub enum StrategyType {
BullCallSpread,
BearCallSpread,
BullPutSpread,
BearPutSpread,
LongButterflySpread,
ShortButterflySpread,
IronCondor,
IronButterfly,
LongStraddle,
ShortStraddle,
LongStrangle,
ShortStrangle,
CoveredCall,
ProtectivePut,
Collar,
LongCall,
LongPut,
ShortCall,
ShortPut,
PoorMansCoveredCall,
CallButterfly,
Custom,
}
impl FromStr for StrategyType {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"BullCallSpread" => Ok(StrategyType::BullCallSpread),
"BearCallSpread" => Ok(StrategyType::BearCallSpread),
"BullPutSpread" => Ok(StrategyType::BullPutSpread),
"BearPutSpread" => Ok(StrategyType::BearPutSpread),
"LongButterflySpread" => Ok(StrategyType::LongButterflySpread),
"ShortButterflySpread" => Ok(StrategyType::ShortButterflySpread),
"IronCondor" => Ok(StrategyType::IronCondor),
"IronButterfly" => Ok(StrategyType::IronButterfly),
"LongStraddle" => Ok(StrategyType::LongStraddle),
"ShortStraddle" => Ok(StrategyType::ShortStraddle),
"LongStrangle" => Ok(StrategyType::LongStrangle),
"ShortStrangle" => Ok(StrategyType::ShortStrangle),
"CoveredCall" => Ok(StrategyType::CoveredCall),
"ProtectivePut" => Ok(StrategyType::ProtectivePut),
"Collar" => Ok(StrategyType::Collar),
"LongCall" => Ok(StrategyType::LongCall),
"LongPut" => Ok(StrategyType::LongPut),
"ShortCall" => Ok(StrategyType::ShortCall),
"ShortPut" => Ok(StrategyType::ShortPut),
"PoorMansCoveredCall" => Ok(StrategyType::PoorMansCoveredCall),
"CallButterfly" => Ok(StrategyType::CallButterfly),
"Custom" => Ok(StrategyType::Custom),
_ => Err(()),
}
}
}
impl StrategyType {
pub fn is_valid(strategy: &str) -> bool {
StrategyType::from_str(strategy).is_ok()
}
}
impl fmt::Display for StrategyType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self:?}")
}
}
pub struct Strategy {
pub name: String,
pub kind: StrategyType,
pub description: String,
pub legs: Vec<Position>,
pub max_profit: Option<f64>,
pub max_loss: Option<f64>,
pub break_even_points: Vec<Positive>,
}
impl Strategy {
pub fn new(name: String, kind: StrategyType, description: String) -> Self {
Strategy {
name,
kind,
description,
legs: Vec::new(),
max_profit: None,
max_loss: None,
break_even_points: Vec::new(),
}
}
}
pub trait BasicAble {
fn get_title(&self) -> String {
unimplemented!("get_title is not implemented for this strategy");
}
fn get_option_basic_type(&self) -> HashSet<OptionBasicType<'_>> {
unimplemented!("get_option_basic_type is not implemented for this strategy");
}
fn get_symbol(&self) -> &str {
self.one_option().get_symbol()
}
fn get_strike(&self) -> HashMap<OptionBasicType<'_>, &Positive> {
self.one_option().get_strike()
}
fn get_strikes(&self) -> Vec<&Positive> {
self.get_option_basic_type()
.iter()
.map(|option_type| option_type.strike_price)
.collect()
}
fn get_side(&self) -> HashMap<OptionBasicType<'_>, &Side> {
self.get_option_basic_type()
.iter()
.map(|option_type| (*option_type, option_type.side))
.collect()
}
fn get_type(&self) -> &OptionType {
self.one_option().get_type()
}
fn get_style(&self) -> HashMap<OptionBasicType<'_>, &OptionStyle> {
self.get_option_basic_type()
.iter()
.map(|option_type| (*option_type, option_type.option_style))
.collect()
}
fn get_expiration(&self) -> HashMap<OptionBasicType<'_>, &ExpirationDate> {
self.get_option_basic_type()
.iter()
.map(|option_type| (*option_type, option_type.expiration_date))
.collect()
}
fn get_implied_volatility(&self) -> HashMap<OptionBasicType<'_>, &Positive> {
unimplemented!("get_implied_volatility is not implemented for this strategy");
}
fn get_quantity(&self) -> HashMap<OptionBasicType<'_>, &Positive> {
unimplemented!("get_quantity is not implemented for this strategy");
}
fn get_underlying_price(&self) -> &Positive {
self.one_option().get_underlying_price()
}
fn get_risk_free_rate(&self) -> HashMap<OptionBasicType<'_>, &Decimal> {
self.one_option().get_risk_free_rate()
}
fn get_dividend_yield(&self) -> HashMap<OptionBasicType<'_>, &Positive> {
self.one_option().get_dividend_yield()
}
fn one_option(&self) -> &Options {
unimplemented!("one_option is not implemented for this strategy");
}
fn one_option_mut(&mut self) -> &mut Options {
unimplemented!("one_option_mut is not implemented for this strategy");
}
fn set_expiration_date(
&mut self,
_expiration_date: ExpirationDate,
) -> Result<(), StrategyError> {
unimplemented!("set_expiration_date is not implemented for this strategy");
}
fn set_underlying_price(&mut self, _price: &Positive) -> Result<(), StrategyError> {
unimplemented!("set_underlying_price is not implemented for this strategy");
}
fn set_implied_volatility(&mut self, _volatility: &Positive) -> Result<(), StrategyError> {
unimplemented!("set_implied_volatility is not implemented for this strategy");
}
}
pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble {
fn get_volume(&mut self) -> Result<Positive, StrategyError> {
let quantities = self.get_quantity();
let mut volume = Positive::ZERO;
for (_, quantity) in quantities {
volume += *quantity;
}
Ok(volume)
}
fn get_max_profit(&self) -> Result<Positive, StrategyError> {
Err(StrategyError::operation_not_supported(
"max_profit",
std::any::type_name::<Self>(),
))
}
fn get_max_profit_mut(&mut self) -> Result<Positive, StrategyError> {
self.get_max_profit()
}
fn get_max_loss(&self) -> Result<Positive, StrategyError> {
Err(StrategyError::operation_not_supported(
"max_loss",
std::any::type_name::<Self>(),
))
}
fn get_max_loss_mut(&mut self) -> Result<Positive, StrategyError> {
self.get_max_loss()
}
fn get_total_cost(&self) -> Result<Positive, PositionError> {
let positions = self.get_positions()?;
let costs = positions
.iter()
.map(|p| p.total_cost().unwrap())
.sum::<Positive>();
Ok(costs)
}
fn get_net_cost(&self) -> Result<Decimal, PositionError> {
let positions = self.get_positions()?;
let costs = positions
.iter()
.map(|p| p.net_cost().unwrap())
.sum::<Decimal>();
Ok(costs)
}
fn get_net_premium_received(&self) -> Result<Positive, StrategyError> {
let positions = self.get_positions()?;
let costs = positions
.iter()
.filter(|p| p.option.side == Side::Long)
.map(|p| p.net_cost().unwrap())
.sum::<Decimal>();
let premiums = positions
.iter()
.filter(|p| p.option.side == Side::Short)
.map(|p| p.net_premium_received().unwrap())
.sum::<Positive>();
match premiums > costs {
true => Ok(premiums - costs),
false => Ok(Positive::ZERO),
}
}
fn get_fees(&self) -> Result<Positive, StrategyError> {
let mut fee = Positive::ZERO;
let positions = match self.get_positions() {
Ok(positions) => positions,
Err(err) => {
return Err(StrategyError::OperationError(
OperationErrorKind::InvalidParameters {
operation: "get_positions".to_string(),
reason: err.to_string(),
},
));
}
};
for position in positions {
fee += position.fees()?;
}
Ok(fee)
}
fn get_profit_area(&self) -> Result<Decimal, StrategyError> {
Err(StrategyError::operation_not_supported(
"profit_area",
std::any::type_name::<Self>(),
))
}
fn get_profit_ratio(&self) -> Result<Decimal, StrategyError> {
Err(StrategyError::operation_not_supported(
"profit_ratio",
std::any::type_name::<Self>(),
))
}
fn get_range_to_show(&self) -> Result<(Positive, Positive), StrategyError> {
let mut all_points = self.get_break_even_points()?.clone();
let (first_strike, last_strike) = self.get_max_min_strikes()?;
let underlying_price = self.get_underlying_price();
let max_diff = (last_strike.value() - underlying_price.value())
.abs()
.max((first_strike.value() - underlying_price.value()).abs());
all_points.push(
(*underlying_price - max_diff)
.max(Positive::ZERO)
.min(first_strike),
);
all_points.push((*underlying_price + max_diff).max(last_strike));
all_points.sort_by(|a, b| a.partial_cmp(b).unwrap());
let start_price = *all_points.first().unwrap() * STRIKE_PRICE_LOWER_BOUND_MULTIPLIER;
let end_price = *all_points.last().unwrap() * STRIKE_PRICE_UPPER_BOUND_MULTIPLIER;
Ok((start_price, end_price))
}
fn get_best_range_to_show(&self, step: Positive) -> Result<Vec<Positive>, StrategyError> {
let (start_price, end_price) = self.get_range_to_show()?;
Ok(calculate_price_range(start_price, end_price, step))
}
fn get_max_min_strikes(&self) -> Result<(Positive, Positive), StrategyError> {
let strikes: Vec<&Positive> = self.get_strikes();
if strikes.is_empty() {
return Err(StrategyError::OperationError(
OperationErrorKind::InvalidParameters {
operation: "max_min_strikes".to_string(),
reason: "No strikes found".to_string(),
},
));
}
let min = strikes.iter().fold(Positive::INFINITY, |acc, &strike| {
Positive::min(acc, *strike)
});
let max = strikes
.iter()
.fold(Positive::ZERO, |acc, &strike| Positive::max(acc, *strike));
let underlying_price = self.get_underlying_price();
let mut min_value = min;
let mut max_value = max;
if underlying_price != &Positive::ZERO {
if min_value > *underlying_price {
min_value = *underlying_price;
}
if *underlying_price > max_value {
max_value = *underlying_price;
}
}
Ok((min_value, max_value))
}
fn get_range_of_profit(&self) -> Result<Positive, StrategyError> {
let mut break_even_points = self.get_break_even_points()?.clone();
match break_even_points.len() {
0 => Err(StrategyError::BreakEvenError(
BreakEvenErrorKind::NoBreakEvenPoints,
)),
1 => Ok(Positive::INFINITY),
2 => Ok(break_even_points[1] - break_even_points[0]),
_ => {
break_even_points.sort_by(|a, b| a.partial_cmp(b).unwrap());
Ok(*break_even_points.last().unwrap() - *break_even_points.first().unwrap())
}
}
}
fn roll_in(&mut self, _position: &Position) -> Result<HashMap<Action, Trade>, StrategyError> {
unimplemented!("roll_in is not implemented for this strategy")
}
fn roll_out(&mut self, _position: &Position) -> Result<HashMap<Action, Trade>, StrategyError> {
unimplemented!("roll_out is not implemented for this strategy")
}
}
pub trait BreakEvenable {
fn get_break_even_points(&self) -> Result<&Vec<Positive>, StrategyError> {
Err(StrategyError::operation_not_supported(
"get_break_even_points",
std::any::type_name::<Self>(),
))
}
fn update_break_even_points(&mut self) -> Result<(), StrategyError> {
unimplemented!("Update break even points is not implemented for this strategy")
}
}
pub trait Validable {
fn validate(&self) -> bool {
panic!("Validate is not applicable for this strategy");
}
}
pub trait Optimizable: Validable + Strategies {
type Strategy: Strategies;
fn get_best_ratio(&mut self, option_chain: &OptionChain, side: FindOptimalSide) {
self.find_optimal(option_chain, side, OptimizationCriteria::Ratio);
}
fn get_best_area(&mut self, option_chain: &OptionChain, side: FindOptimalSide) {
self.find_optimal(option_chain, side, OptimizationCriteria::Area);
}
fn filter_combinations<'a>(
&'a self,
_option_chain: &'a OptionChain,
_side: FindOptimalSide,
) -> impl Iterator<Item = OptionDataGroup<'a>> {
error!("Filter combinations is not applicable for this strategy");
std::iter::empty()
}
fn find_optimal(
&mut self,
_option_chain: &OptionChain,
_side: FindOptimalSide,
_criteria: OptimizationCriteria,
) {
panic!("Find optimal is not applicable for this strategy");
}
fn is_valid_optimal_option(&self, option: &OptionData, side: &FindOptimalSide) -> bool {
match side {
FindOptimalSide::Upper => option.strike_price >= *self.get_underlying_price(),
FindOptimalSide::Lower => option.strike_price <= *self.get_underlying_price(),
FindOptimalSide::All => true,
FindOptimalSide::Range(start, end) => {
option.strike_price >= *start && option.strike_price <= *end
}
FindOptimalSide::Deltable(_threshold) => true,
FindOptimalSide::Center => {
panic!("Center should be managed by the strategy");
}
FindOptimalSide::DeltaRange(min, max) => {
let (delta_call, delta_put) = option.current_deltas();
(delta_put.is_some() && delta_put.unwrap() >= *min && delta_put.unwrap() <= *max)
|| (delta_call.is_some()
&& delta_call.unwrap() >= *min
&& delta_call.unwrap() <= *max)
}
}
}
fn are_valid_legs(&self, legs: &StrategyLegs) -> bool {
let (long, short) = match legs {
StrategyLegs::TwoLegs { first, second } => (first, second),
_ => panic!("Invalid number of legs for this strategy"),
};
long.call_ask.unwrap_or(Positive::ZERO) > Positive::ZERO
&& short.call_bid.unwrap_or(Positive::ZERO) > Positive::ZERO
}
fn create_strategy(&self, _chain: &OptionChain, _legs: &StrategyLegs) -> Self::Strategy {
unimplemented!("Create strategy is not applicable for this strategy");
}
}
pub trait Positionable {
fn add_position(&mut self, _position: &Position) -> Result<(), PositionError> {
Err(PositionError::unsupported_operation(
std::any::type_name::<Self>(),
"add_position",
))
}
fn get_positions(&self) -> Result<Vec<&Position>, PositionError> {
Err(PositionError::unsupported_operation(
std::any::type_name::<Self>(),
"get_positions",
))
}
fn get_position(
&mut self,
_option_style: &OptionStyle,
_side: &Side,
_strike: &Positive,
) -> Result<Vec<&mut Position>, PositionError> {
unimplemented!("Modify position is not implemented for this strategy")
}
fn get_position_unique(
&mut self,
_option_style: &OptionStyle,
_side: &Side,
) -> Result<&mut Position, PositionError> {
unimplemented!("Get unique position is not implemented for this strategy")
}
fn get_option_unique(
&mut self,
_option_style: &OptionStyle,
_side: &Side,
) -> Result<&mut Options, PositionError> {
unimplemented!("Get unique option is not implemented for this strategy")
}
fn modify_position(&mut self, _position: &Position) -> Result<(), PositionError> {
unimplemented!("Modify position is not implemented for this strategy")
}
fn replace_position(&mut self, _position: &Position) -> Result<(), PositionError> {
unimplemented!("Replace position is not implemented for this strategy")
}
fn valid_premium_for_shorts(&self, min_premium: &Positive) -> bool {
let positions = match self.get_positions() {
Ok(positions) => positions,
Err(_) => return false,
};
positions
.iter()
.filter(|position| position.is_short())
.all(|p| {
p.net_premium_received()
.is_ok_and(|premium| premium >= *min_premium)
})
}
}
#[cfg(test)]
mod tests_strategies_extended {
use super::*;
use positive::pos_or_panic;
use crate::model::position::Position;
use crate::model::types::{OptionStyle, Side};
use crate::model::utils::create_sample_option_simplest;
#[test]
fn test_strategy_enum() {
assert_ne!(StrategyType::BullCallSpread, StrategyType::BearCallSpread);
}
#[test]
fn test_strategy_new_with_legs() {
let mut strategy = Strategy::new(
"Test Strategy".to_string(),
StrategyType::BullCallSpread,
"Test Description".to_string(),
);
let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
let position = Position::new(
option,
Positive::ONE,
Default::default(),
Positive::ZERO,
Positive::ZERO,
None,
None,
);
strategy.legs.push(position);
assert_eq!(strategy.legs.len(), 1);
}
#[test]
fn test_strategies_get_legs_panic() {
struct PanicStrategy;
impl Validable for PanicStrategy {}
impl Positionable for PanicStrategy {}
impl BreakEvenable for PanicStrategy {}
impl BasicAble for PanicStrategy {}
impl Strategies for PanicStrategy {
fn get_volume(&mut self) -> Result<Positive, StrategyError> {
unreachable!()
}
}
let strategy = PanicStrategy;
assert!(strategy.get_positions().is_err());
}
#[test]
fn test_strategies_break_even_panic() {
struct PanicStrategy;
impl Validable for PanicStrategy {}
impl Positionable for PanicStrategy {}
impl BreakEvenable for PanicStrategy {}
impl BasicAble for PanicStrategy {}
impl Strategies for PanicStrategy {
fn get_volume(&mut self) -> Result<Positive, StrategyError> {
unreachable!()
}
}
let strategy = PanicStrategy;
assert!(strategy.get_break_even_points().is_err());
}
#[test]
fn test_strategies_net_premium_received_panic() {
struct PanicStrategy;
impl Validable for PanicStrategy {}
impl Positionable for PanicStrategy {}
impl BreakEvenable for PanicStrategy {}
impl BasicAble for PanicStrategy {}
impl Strategies for PanicStrategy {}
let strategy = PanicStrategy;
assert!(strategy.get_net_premium_received().is_err());
}
#[test]
fn test_strategies_fees_panic() {
struct PanicStrategy;
impl Validable for PanicStrategy {}
impl Positionable for PanicStrategy {}
impl BreakEvenable for PanicStrategy {}
impl BasicAble for PanicStrategy {}
impl Strategies for PanicStrategy {}
let strategy = PanicStrategy;
assert!(strategy.get_fees().is_err());
}
#[test]
fn test_strategies_max_profit_iter() {
struct TestStrategy;
impl Validable for TestStrategy {}
impl Positionable for TestStrategy {}
impl BreakEvenable for TestStrategy {}
impl BasicAble for TestStrategy {}
impl Strategies for TestStrategy {
fn get_max_profit(&self) -> Result<Positive, StrategyError> {
Ok(Positive::HUNDRED)
}
}
let mut strategy = TestStrategy;
assert_eq!(strategy.get_max_profit_mut().unwrap().to_f64(), 100.0);
}
#[test]
fn test_strategies_max_loss_iter() {
struct TestStrategy;
impl Validable for TestStrategy {}
impl Positionable for TestStrategy {}
impl BreakEvenable for TestStrategy {}
impl BasicAble for TestStrategy {}
impl Strategies for TestStrategy {
fn get_max_loss(&self) -> Result<Positive, StrategyError> {
Ok(pos_or_panic!(50.0))
}
}
let mut strategy = TestStrategy;
assert_eq!(strategy.get_max_loss_mut().unwrap().to_f64(), 50.0);
}
#[test]
fn test_strategies_empty_strikes() {
struct EmptyStrategy;
impl Validable for EmptyStrategy {}
impl Positionable for EmptyStrategy {
fn get_positions(&self) -> Result<Vec<&Position>, PositionError> {
Ok(vec![])
}
}
impl BreakEvenable for EmptyStrategy {}
impl BasicAble for EmptyStrategy {
fn get_option_basic_type(&self) -> HashSet<OptionBasicType<'_>> {
HashSet::new()
}
}
impl Strategies for EmptyStrategy {}
let strategy = EmptyStrategy;
assert_eq!(strategy.get_strikes(), Vec::<&Positive>::new());
assert!(strategy.get_max_min_strikes().is_err());
}
}
#[cfg(test)]
mod tests_strategy_type {
use super::*;
#[test]
fn test_strategy_type_equality() {
assert_eq!(StrategyType::BullCallSpread, StrategyType::BullCallSpread);
assert_ne!(StrategyType::BullCallSpread, StrategyType::BearCallSpread);
}
#[test]
fn test_strategy_type_clone() {
let strategy = StrategyType::IronCondor;
let cloned = strategy.clone();
assert_eq!(strategy, cloned);
}
#[test]
fn test_strategy_type_debug() {
let strategy = StrategyType::ShortStraddle;
let debug_string = format!("{strategy:?}");
assert_eq!(debug_string, "ShortStraddle");
}
#[test]
fn test_strategy_type_from_str() {
assert_eq!(
StrategyType::from_str("ShortStrangle"),
Ok(StrategyType::ShortStrangle)
);
assert_eq!(
StrategyType::from_str("LongCall"),
Ok(StrategyType::LongCall)
);
assert_eq!(
StrategyType::from_str("BullCallSpread"),
Ok(StrategyType::BullCallSpread)
);
assert_eq!(StrategyType::from_str("InvalidStrategy"), Err(()));
}
#[test]
fn test_strategy_type_is_valid() {
assert!(StrategyType::is_valid("ShortStrangle"));
assert!(StrategyType::is_valid("LongPut"));
assert!(StrategyType::is_valid("CoveredCall"));
assert!(!StrategyType::is_valid("InvalidStrategy"));
assert!(!StrategyType::is_valid("Random"));
}
#[test]
fn test_strategy_type_serialization() {
let strategy = StrategyType::IronCondor;
let serialized = serde_json::to_string(&strategy).unwrap();
assert_eq!(serialized, "\"IronCondor\"");
let deserialized: StrategyType = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized, StrategyType::IronCondor);
}
#[test]
fn test_strategy_type_deserialization() {
let json_data = "\"ShortStraddle\"";
let deserialized: StrategyType = serde_json::from_str(json_data).unwrap();
assert_eq!(deserialized, StrategyType::ShortStraddle);
}
#[test]
fn test_invalid_strategy_type_deserialization() {
let json_data = "\"InvalidStrategy\"";
let deserialized: Result<StrategyType, _> = serde_json::from_str(json_data);
assert!(deserialized.is_err());
}
}
#[cfg(test)]
mod tests_best_range_to_show {
use super::*;
use positive::pos_or_panic;
struct TestStrategy {
underlying_price: Positive,
break_even_points: Vec<Positive>,
}
impl TestStrategy {
fn new(underlying_price: Positive, break_even_points: Vec<Positive>) -> Self {
Self {
underlying_price,
break_even_points,
}
}
}
impl Validable for TestStrategy {}
impl Positionable for TestStrategy {}
impl BreakEvenable for TestStrategy {
fn get_break_even_points(&self) -> Result<&Vec<Positive>, StrategyError> {
Ok(&self.break_even_points)
}
}
impl BasicAble for TestStrategy {
fn get_underlying_price(&self) -> &Positive {
&self.underlying_price
}
fn get_option_basic_type(&self) -> HashSet<OptionBasicType<'_>> {
HashSet::new()
}
}
impl Strategies for TestStrategy {
fn get_max_min_strikes(&self) -> Result<(Positive, Positive), StrategyError> {
Ok((pos_or_panic!(90.0), Positive::HUNDRED))
}
}
#[test]
fn test_basic_range_with_step() {
let strategy = TestStrategy::new(
Positive::HUNDRED,
vec![pos_or_panic!(90.0), pos_or_panic!(110.0)],
);
let range = strategy.get_best_range_to_show(pos_or_panic!(5.0)).unwrap();
assert!(!range.is_empty());
assert_eq!(range[1] - range[0], pos_or_panic!(5.0));
}
#[test]
fn test_range_with_small_step() {
let strategy = TestStrategy::new(
Positive::HUNDRED,
vec![pos_or_panic!(95.0), pos_or_panic!(105.0)],
);
let range = strategy.get_best_range_to_show(Positive::ONE).unwrap();
assert!(!range.is_empty());
assert_eq!(range[1] - range[0], Positive::ONE);
}
#[test]
fn test_range_boundaries() {
let strategy = TestStrategy::new(
Positive::HUNDRED,
vec![pos_or_panic!(90.0), pos_or_panic!(110.0)],
);
let range = strategy.get_best_range_to_show(pos_or_panic!(5.0)).unwrap();
assert!(range.first().unwrap() < &pos_or_panic!(90.0));
assert!(range.last().unwrap() > &pos_or_panic!(110.0));
}
#[test]
fn test_range_step_size() {
let strategy = TestStrategy::new(
Positive::HUNDRED,
vec![pos_or_panic!(90.0), pos_or_panic!(110.0)],
);
let step = pos_or_panic!(5.0);
let range = strategy.get_best_range_to_show(step).unwrap();
for i in 1..range.len() {
assert_eq!(range[i] - range[i - 1], step);
}
}
#[test]
fn test_range_includes_underlying() {
let underlying_price = Positive::HUNDRED;
let strategy = TestStrategy::new(
underlying_price,
vec![pos_or_panic!(90.0), pos_or_panic!(110.0)],
);
let range = strategy.get_best_range_to_show(pos_or_panic!(5.0)).unwrap();
assert!(range.iter().any(|&price| price <= underlying_price));
assert!(range.iter().any(|&price| price >= underlying_price));
}
#[test]
fn test_range_with_extreme_values() {
let strategy = TestStrategy::new(
Positive::HUNDRED,
vec![pos_or_panic!(50.0), pos_or_panic!(150.0)],
);
let range = strategy
.get_best_range_to_show(pos_or_panic!(10.0))
.unwrap();
assert!(range.first().unwrap() <= &pos_or_panic!(50.0));
assert!(range.last().unwrap() >= &pos_or_panic!(150.0));
}
}
#[cfg(test)]
mod tests_range_to_show {
use super::*;
use positive::pos_or_panic;
struct TestStrategy {
underlying_price: Positive,
break_even_points: Vec<Positive>,
}
impl TestStrategy {
fn new(underlying_price: Positive, break_even_points: Vec<Positive>) -> Self {
Self {
underlying_price,
break_even_points,
}
}
}
impl Validable for TestStrategy {}
impl Positionable for TestStrategy {}
impl BreakEvenable for TestStrategy {
fn get_break_even_points(&self) -> Result<&Vec<Positive>, StrategyError> {
Ok(&self.break_even_points)
}
}
impl BasicAble for TestStrategy {
fn get_option_basic_type(&self) -> HashSet<OptionBasicType<'_>> {
HashSet::new()
}
fn get_underlying_price(&self) -> &Positive {
&self.underlying_price
}
}
impl Strategies for TestStrategy {
fn get_max_min_strikes(&self) -> Result<(Positive, Positive), StrategyError> {
Ok((pos_or_panic!(90.0), pos_or_panic!(110.0)))
}
}
#[test]
fn test_basic_range() {
let strategy = TestStrategy::new(
Positive::HUNDRED,
vec![pos_or_panic!(90.0), pos_or_panic!(110.0)],
);
let (start, end) = strategy.get_range_to_show().unwrap();
assert!(start < pos_or_panic!(90.0));
assert!(end > pos_or_panic!(110.0));
}
#[test]
fn test_range_with_far_strikes() {
let strategy = TestStrategy::new(
Positive::HUNDRED,
vec![pos_or_panic!(90.0), pos_or_panic!(110.0)],
);
let (start, end) = strategy.get_range_to_show().unwrap();
assert!(start < pos_or_panic!(90.0));
assert!(end > pos_or_panic!(110.0));
}
#[test]
fn test_range_with_underlying_outside_strikes() {
let strategy = TestStrategy::new(
pos_or_panic!(150.0),
vec![pos_or_panic!(90.0), pos_or_panic!(110.0)],
);
let (_start, end) = strategy.get_range_to_show().unwrap();
assert!(end > pos_or_panic!(150.0));
}
}
#[cfg(test)]
mod tests_range_of_profit {
use super::*;
use positive::pos_or_panic;
struct TestStrategy {
break_even_points: Vec<Positive>,
}
impl TestStrategy {
fn new(break_even_points: Vec<Positive>) -> Self {
Self { break_even_points }
}
}
impl Validable for TestStrategy {}
impl Positionable for TestStrategy {}
impl BreakEvenable for TestStrategy {
fn get_break_even_points(&self) -> Result<&Vec<Positive>, StrategyError> {
Ok(&self.break_even_points)
}
}
impl BasicAble for TestStrategy {}
impl Strategies for TestStrategy {}
#[test]
fn test_no_break_even_points() {
let strategy = TestStrategy::new(vec![]);
assert!(strategy.get_range_of_profit().is_err());
}
#[test]
fn test_single_break_even_point() {
let strategy = TestStrategy::new(vec![Positive::HUNDRED]);
assert_eq!(strategy.get_range_of_profit().unwrap(), Positive::INFINITY);
}
#[test]
fn test_two_break_even_points() {
let strategy = TestStrategy::new(vec![pos_or_panic!(90.0), pos_or_panic!(110.0)]);
assert_eq!(strategy.get_range_of_profit().unwrap(), pos_or_panic!(20.0));
}
#[test]
fn test_multiple_break_even_points() {
let strategy = TestStrategy::new(vec![
pos_or_panic!(80.0),
Positive::HUNDRED,
pos_or_panic!(120.0),
]);
assert_eq!(strategy.get_range_of_profit().unwrap(), pos_or_panic!(40.0));
}
#[test]
fn test_unordered_break_even_points() {
let strategy = TestStrategy::new(vec![
pos_or_panic!(120.0),
pos_or_panic!(80.0),
Positive::HUNDRED,
]);
assert_eq!(strategy.get_range_of_profit().unwrap(), pos_or_panic!(40.0));
}
}
#[cfg(test)]
mod tests_strategy_methods {
use super::*;
#[test]
fn test_get_underlying_price_panic() {
struct TestStrategy;
impl Validable for TestStrategy {}
impl Positionable for TestStrategy {}
impl BreakEvenable for TestStrategy {}
impl BasicAble for TestStrategy {}
impl Strategies for TestStrategy {}
let strategy = TestStrategy;
let result = std::panic::catch_unwind(|| strategy.get_underlying_price());
assert!(result.is_err());
}
}
#[cfg(test)]
mod tests_optimizable {
use super::*;
use positive::{pos_or_panic, spos};
use crate::chains::OptionData;
use rust_decimal_macros::dec;
struct TestOptimizableStrategy;
impl Validable for TestOptimizableStrategy {
fn validate(&self) -> bool {
true
}
}
impl Positionable for TestOptimizableStrategy {}
impl BreakEvenable for TestOptimizableStrategy {}
impl BasicAble for TestOptimizableStrategy {}
impl Strategies for TestOptimizableStrategy {}
impl Optimizable for TestOptimizableStrategy {
type Strategy = Self;
}
#[test]
fn test_is_valid_long_option() {
let strategy = TestOptimizableStrategy;
let option_data = OptionData::new(
Positive::HUNDRED, spos!(5.0), spos!(5.5), spos!(4.0), spos!(4.5), pos_or_panic!(0.2), Some(dec!(0.5)), Some(dec!(0.3)),
Some(dec!(0.3)),
spos!(1000.0), Some(100), None,
None,
None,
None,
None,
None,
None,
);
assert!(strategy.is_valid_optimal_option(&option_data, &FindOptimalSide::All));
assert!(strategy.is_valid_optimal_option(
&option_data,
&FindOptimalSide::Range(pos_or_panic!(90.0), pos_or_panic!(110.0))
));
}
#[test]
#[should_panic]
fn test_is_valid_long_option_upper_panic() {
let strategy = TestOptimizableStrategy;
let option_data = OptionData::new(
Positive::HUNDRED, spos!(5.0), spos!(5.5), spos!(4.0), spos!(4.5), pos_or_panic!(0.2), Some(dec!(0.5)), Some(dec!(0.3)),
Some(dec!(0.3)),
spos!(1000.0), Some(100), None,
None,
None,
None,
None,
None,
None,
);
assert!(strategy.is_valid_optimal_option(&option_data, &FindOptimalSide::Upper));
}
#[test]
#[should_panic]
fn test_is_valid_long_option_lower_panic() {
let strategy = TestOptimizableStrategy;
let option_data = OptionData::new(
Positive::HUNDRED, spos!(5.0), spos!(5.5), spos!(4.0), spos!(4.5), pos_or_panic!(0.2), Some(dec!(0.5)), Some(dec!(0.3)),
Some(dec!(0.3)),
spos!(1000.0), Some(100), None,
None,
None,
None,
None,
None,
None,
);
assert!(strategy.is_valid_optimal_option(&option_data, &FindOptimalSide::Lower));
}
#[test]
fn test_is_valid_short_option() {
let strategy = TestOptimizableStrategy;
let option_data = OptionData::new(
Positive::HUNDRED, spos!(5.0), spos!(5.5), spos!(4.0), spos!(4.5), pos_or_panic!(0.2), Some(dec!(0.5)), Some(dec!(0.3)),
Some(dec!(0.3)),
spos!(1000.0), Some(100), None,
None,
None,
None,
None,
None,
None,
);
assert!(strategy.is_valid_optimal_option(&option_data, &FindOptimalSide::All));
assert!(strategy.is_valid_optimal_option(
&option_data,
&FindOptimalSide::Range(pos_or_panic!(90.0), pos_or_panic!(110.0))
));
}
#[test]
#[should_panic]
fn test_is_valid_short_option_upper_panic() {
let strategy = TestOptimizableStrategy;
let option_data = OptionData::new(
Positive::HUNDRED, spos!(5.0), spos!(5.5), spos!(4.0), spos!(4.5), pos_or_panic!(0.2), Some(dec!(0.5)), Some(dec!(0.3)),
Some(dec!(0.3)),
spos!(1000.0), Some(100), None,
None,
None,
None,
None,
None,
None,
);
assert!(strategy.is_valid_optimal_option(&option_data, &FindOptimalSide::Upper));
}
#[test]
#[should_panic]
fn test_is_valid_short_option_lower_panic() {
let strategy = TestOptimizableStrategy;
let option_data = OptionData::new(
Positive::HUNDRED, spos!(5.0), spos!(5.5), spos!(4.0), spos!(4.5), pos_or_panic!(0.2), Some(dec!(0.5)), Some(dec!(0.3)),
Some(dec!(0.3)),
spos!(1000.0), Some(100), None,
None,
None,
None,
None,
None,
None,
);
assert!(strategy.is_valid_optimal_option(&option_data, &FindOptimalSide::Lower));
}
}
#[cfg(test)]
mod tests_strategy_net_operations {
use super::*;
use crate::model::position::Position;
use crate::model::types::{OptionStyle, Side};
use crate::model::utils::create_sample_option_simplest;
use chrono::{TimeZone, Utc};
use positive::pos_or_panic;
struct TestStrategy {
positions: Vec<Position>,
}
impl TestStrategy {
fn new() -> Self {
Self {
positions: Vec::new(),
}
}
}
impl Validable for TestStrategy {}
impl Positionable for TestStrategy {
fn add_position(&mut self, position: &Position) -> Result<(), PositionError> {
self.positions.push(position.clone());
Ok(())
}
fn get_positions(&self) -> Result<Vec<&Position>, PositionError> {
Ok(self.positions.iter().collect())
}
}
impl BreakEvenable for TestStrategy {}
impl BasicAble for TestStrategy {}
impl Strategies for TestStrategy {}
#[test]
fn test_net_cost_calculation() {
let mut strategy = TestStrategy::new();
let option_long = create_sample_option_simplest(OptionStyle::Call, Side::Long);
let option_short = create_sample_option_simplest(OptionStyle::Call, Side::Short);
let position_long = Position::new(
option_long,
Positive::ONE,
Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
Positive::ONE,
pos_or_panic!(0.5),
None,
None,
);
let position_short = Position::new(
option_short,
Positive::ONE,
Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
Positive::ONE,
pos_or_panic!(0.5),
None,
None,
);
strategy.add_position(&position_long).unwrap();
strategy.add_position(&position_short).unwrap();
let result = strategy.get_net_cost().unwrap();
assert!(result > Decimal::ZERO);
}
#[test]
fn test_net_premium_received_calculation() {
let mut strategy = TestStrategy::new();
let option_long = create_sample_option_simplest(OptionStyle::Call, Side::Long);
let option_short = create_sample_option_simplest(OptionStyle::Call, Side::Short);
let fixed_date = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
let position_long = Position::new(
option_long,
Positive::ONE,
fixed_date,
Positive::ONE,
pos_or_panic!(0.5),
None,
None,
);
let position_short = Position::new(
option_short,
Positive::ONE,
fixed_date,
Positive::ONE,
pos_or_panic!(0.5),
None,
None,
);
strategy.add_position(&position_long).unwrap();
strategy.add_position(&position_short).unwrap();
let result = strategy.get_net_premium_received().unwrap();
assert_eq!(result, Positive::ZERO);
}
#[test]
fn test_fees_calculation() {
let mut strategy = TestStrategy::new();
let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
let fixed_date = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
let position = Position::new(
option,
Positive::ONE,
fixed_date,
Positive::ONE,
pos_or_panic!(0.5),
None,
None,
);
strategy.add_position(&position).unwrap();
let result = strategy.get_fees().unwrap();
assert!(result > Positive::ZERO);
}
}