#![allow(clippy::indexing_slicing)]
use crate::pricing::Profit;
use crate::strategies::base::BreakEvenable;
use crate::strategies::{
BasicAble, BearCallSpread, BearPutSpread, BullCallSpread, BullPutSpread, CallButterfly,
IronButterfly, IronCondor, LongButterflySpread, LongCall, LongPut, LongStraddle, LongStrangle,
PoorMansCoveredCall, ShortButterflySpread, ShortCall, ShortPut, ShortStraddle, ShortStrangle,
Strategies,
};
use crate::visualization::{
ColorScheme, Graph, GraphConfig, GraphData, Label2D, LineStyle, Series2D, TraceMode, VisPoint2D,
};
use rust_decimal::Decimal;
#[macro_export]
macro_rules! impl_graph_for_payoff_strategy {
($($t:ty),*) => {
$(
impl Graph for $t {
fn graph_data(&self) -> GraphData {
let break_even_points = match self.get_break_even_points() {
Ok(points) => points,
Err(_) => return GraphData::Series(Series2D::default()),
};
let underlying_price = self.get_underlying_price();
let pay_off_at_underlying_price = self.calculate_profit_at(&underlying_price).unwrap_or(Decimal::ZERO);
let range = match self.get_best_range_to_show(positive::Positive::ONE){
Ok(range) => range,
Err(_) => return GraphData::Series(Series2D::default()),
};
let mut break_even_labels = Vec::new();
let mut profit_series = Series2D {
x: vec![],
y: vec![],
name: "Profit/Loss".to_string(),
mode: TraceMode::Lines,
line_color: Some("#2ca02c".to_string()),
line_width: Some(2.0),
};
let mut zero_line = Series2D {
x: vec![],
y: vec![],
name: "Break Even".to_string(),
mode: TraceMode::Lines,
line_color: Some("#000000".to_string()),
line_width: Some(1.0),
};
let mut current_price_line = Series2D {
x: vec![],
y: vec![],
name: "Current Price".to_string(),
mode: TraceMode::Lines,
line_color: Some("#0000FF".to_string()), line_width: Some(1.0),
};
let point_color = if pay_off_at_underlying_price > Decimal::ZERO {
"#2ca02c".to_string() } else if pay_off_at_underlying_price < Decimal::ZERO {
"#FF0000".to_string() } else {
"#0000FF".to_string() };
let formatted_payoff = format!("{:.2}", pay_off_at_underlying_price);
let current_point = Series2D {
x: vec![underlying_price.to_dec()],
y: vec![pay_off_at_underlying_price],
name: format!("Current P/L: {}", formatted_payoff),
mode: TraceMode::Markers,
line_color: Some(point_color.clone()),
line_width: Some(10.0), };
let mut y_offset = Decimal::ZERO;
let label_point = VisPoint2D {
x: underlying_price.to_dec(),
y: y_offset,
name: format!("P/L Label"),
mode: TraceMode::TextLabels,
color: Some(point_color),
width: Some(0.0),
};
let label = Label2D {
point: label_point,
label: formatted_payoff,
};
let mut label_series = Series2D {
x: vec![label.point.x],
y: vec![label.point.y],
name: label.label.clone(),
mode: TraceMode::TextLabels,
line_color: label.point.color,
line_width: label.point.width,
};
if let (Some(first), Some(last)) = (range.first(), range.last()) {
let min_price = first.to_dec();
let max_price = last.to_dec();
zero_line.x.push(min_price);
zero_line.y.push(Decimal::ZERO);
zero_line.x.push(max_price);
zero_line.y.push(Decimal::ZERO);
let mut min_profit = Decimal::MAX;
let mut max_profit = Decimal::MIN;
for price in range.iter() {
if let Ok(profit) = self.calculate_profit_at(price) {
if profit < min_profit {
min_profit = profit;
}
if profit > max_profit {
max_profit = profit;
}
}
}
let padding = (max_profit - min_profit) * Decimal::new(5, 2);
if pay_off_at_underlying_price >= Decimal::ZERO {
y_offset = max_profit + padding;
} else {
y_offset = min_profit - padding;
}
label_series.y[0] = y_offset;
for be_point in break_even_points.iter() {
let be_label_point = VisPoint2D {
x: be_point.to_dec(),
y: max_profit + padding * Decimal::new(5, 1), name: format!("Break-even"),
mode: TraceMode::TextLabels,
color: Some("#000000".to_string()), width: Some(0.0),
};
let be_label = Label2D {
point: be_label_point,
label: format!("BE: {}", be_point.to_dec().round_dp(2)),
};
break_even_labels.push(be_label);
}
current_price_line.x.push(underlying_price.to_dec());
current_price_line.y.push(min_profit - padding);
current_price_line.x.push(underlying_price.to_dec());
current_price_line.y.push(max_profit + padding);
}
for price in range {
let profit = match self.calculate_profit_at(&price) {
Ok(p) => p,
Err(e) => {
::tracing::warn!(
error = %e,
"skipping price point with unscorable profit"
);
continue;
}
};
profit_series.x.push(price.to_dec());
profit_series.y.push(profit);
}
let mut segments = Vec::new();
let mut current_segment = Vec::new();
let mut current_sign: Option<i8> = None;
for (i, price) in profit_series.x.iter().enumerate() {
let profit = profit_series.y[i];
let sign = if profit > Decimal::ZERO {
1
} else if profit < Decimal::ZERO {
-1
} else {
0
};
let sign_changed = match current_sign {
None => true,
Some(cur) => sign != 0 && cur != sign,
};
if sign_changed {
if !current_segment.is_empty() {
if let Some(cur) = current_sign {
segments.push((current_segment, cur));
}
current_segment = Vec::new();
}
current_sign = Some(sign);
}
current_segment.push((*price, profit));
}
if let (false, Some(cur)) = (current_segment.is_empty(), current_sign) {
segments.push((current_segment, cur));
}
let mut series_list = Vec::new();
for (i, (segment, sign)) in segments.iter().enumerate() {
let color = if *sign > 0 {
"#2ca02c".to_string() } else if *sign < 0 {
"#FF0000".to_string() } else {
"#000000".to_string() };
let series = Series2D {
x: segment.iter().map(|(x, _)| *x).collect(),
y: segment.iter().map(|(_, y)| *y).collect(),
name: format!("Segment {}", i + 1),
mode: TraceMode::Lines,
line_color: Some(color),
line_width: Some(2.0),
};
series_list.push(series);
}
series_list.push(zero_line);
series_list.push(current_price_line);
series_list.push(current_point);
series_list.push(label_series);
for be_label in break_even_labels {
let be_series = Series2D {
x: vec![be_label.point.x],
y: vec![be_label.point.y],
name: be_label.label.clone(),
mode: TraceMode::TextLabels,
line_color: be_label.point.color,
line_width: be_label.point.width,
};
series_list.push(be_series);
}
GraphData::MultiSeries(series_list)
}
fn graph_config(&self) -> GraphConfig {
let title = self.get_title();
let legend = Some(vec![title.clone()]);
GraphConfig {
title,
width: 1600,
height: 900,
x_label: Some("Underlying Price".to_string()),
y_label: Some("Profit/Loss".to_string()),
z_label: None,
line_style: LineStyle::Solid,
color_scheme: ColorScheme::Default,
legend,
show_legend: false,
}
}
}
)*
};
}
impl_graph_for_payoff_strategy!(
BullCallSpread,
BearCallSpread,
BullPutSpread,
BearPutSpread,
LongButterflySpread,
ShortButterflySpread,
IronCondor,
IronButterfly,
LongStraddle,
ShortStraddle,
LongStrangle,
ShortStrangle,
LongCall,
LongPut,
ShortCall,
ShortPut,
PoorMansCoveredCall,
CallButterfly,
crate::strategies::custom::CustomStrategy,
crate::strategies::covered_call::CoveredCall,
crate::strategies::collar::Collar,
crate::strategies::protective_put::ProtectivePut
);