use optionstratlib_strategies::strategies::{
BearCallSpread, BearPutSpread, BullCallLadder, BullCallSpread, BullPutSpread, IronButterfly,
IronCondor, LongButterflySpread, LongCall, LongPut, LongStraddle, LongStrangle,
PoorMansCoveredCall, ShortButterflySpread, ShortCall, ShortPut, ShortStraddle, ShortStrangle,
};
#[macro_export]
macro_rules! impl_graph_for_payoff_strategy {
($($t:ty),*) => {
$(
impl $crate::visualization::Graph for $t {
fn graph_data(&self) -> $crate::visualization::GraphData {
use $crate::__private::{
BasicAble, BreakEvenable, Decimal, Positive, Profit, Strategies,
};
use $crate::visualization::{
GraphData, Label2D, Series2D, TraceMode, VisPoint2D,
};
let break_even_points = match self.get_break_even_points() {
Ok(points) => points,
Err(_) => return GraphData::Series(Series2D::default()),
};
let underlying_price = match self.get_underlying_price() {
Ok(price) => price,
Err(_) => return GraphData::Series(Series2D::default()),
};
let pay_off_at_underlying_price = self.calculate_profit_at(&underlying_price).ok();
let range = match self.get_best_range_to_show(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 mut marker = pay_off_at_underlying_price.map(|pay_off_at_underlying_price| {
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 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 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,
};
(current_point, label_series)
});
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 extent = if min_profit <= max_profit {
max_profit
.checked_sub(min_profit)
.and_then(|span| span.checked_mul(Decimal::new(5, 2)))
.and_then(|padding| {
Some((
min_profit.checked_sub(padding)?,
max_profit.checked_add(padding)?,
max_profit
.checked_add(padding.checked_mul(Decimal::new(5, 1))?)?,
))
})
} else {
None
};
let Some((padded_min, padded_max, label_height)) = extent else {
$crate::__private::tracing::warn!(
%min_profit,
%max_profit,
"payoff chart has no drawable vertical extent"
);
return GraphData::Series(Series2D::default());
};
if let Some((payoff, (_, label_series))) =
pay_off_at_underlying_price.zip(marker.as_mut())
{
let y_offset = if payoff >= Decimal::ZERO {
padded_max
} else {
padded_min
};
if let Some(label_y) = label_series.y.first_mut() {
*label_y = y_offset;
}
}
for be_point in break_even_points.iter() {
let be_label_point = VisPoint2D {
x: be_point.to_dec(),
y: label_height, 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(padded_min);
current_price_line.x.push(underlying_price.to_dec());
current_price_line.y.push(padded_max);
}
for price in range {
let profit = match self.calculate_profit_at(&price) {
Ok(p) => p,
Err(e) => {
$crate::__private::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 (price, &profit) in profit_series.x.iter().zip(&profit_series.y) {
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 ((segment, sign), number) in segments.iter().zip(1usize..) {
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 {number}"),
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);
if let Some((current_point, label_series)) = marker {
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) -> $crate::visualization::GraphConfig {
use $crate::__private::BasicAble;
use $crate::visualization::{ColorScheme, GraphConfig, LineStyle};
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,
BullCallLadder,
optionstratlib_strategies::strategies::custom::CustomStrategy,
optionstratlib_strategies::strategies::covered_call::CoveredCall,
optionstratlib_strategies::strategies::collar::Collar,
optionstratlib_strategies::strategies::protective_put::ProtectivePut
);
#[cfg(test)]
mod tests {
use crate::visualization::Graph;
use optionstratlib_strategies::strategies::collar::Collar;
use optionstratlib_strategies::strategies::covered_call::CoveredCall;
use optionstratlib_strategies::strategies::custom::CustomStrategy;
use optionstratlib_strategies::strategies::protective_put::ProtectivePut;
use optionstratlib_strategies::strategies::{
BearCallSpread, BearPutSpread, BullCallLadder, BullCallSpread, BullPutSpread,
IronButterfly, IronCondor, LongButterflySpread, LongCall, LongPut, LongStraddle,
LongStrangle, PoorMansCoveredCall, ShortButterflySpread, ShortCall, ShortPut,
ShortStraddle, ShortStrangle, Strategable,
};
use static_assertions::assert_impl_all;
#[test]
fn test_every_strategy_implements_graph() {
assert_impl_all!(BullCallSpread: Strategable, Graph);
assert_impl_all!(BearCallSpread: Strategable, Graph);
assert_impl_all!(BullPutSpread: Strategable, Graph);
assert_impl_all!(BearPutSpread: Strategable, Graph);
assert_impl_all!(LongButterflySpread: Strategable, Graph);
assert_impl_all!(ShortButterflySpread: Strategable, Graph);
assert_impl_all!(IronCondor: Strategable, Graph);
assert_impl_all!(IronButterfly: Strategable, Graph);
assert_impl_all!(LongStraddle: Strategable, Graph);
assert_impl_all!(ShortStraddle: Strategable, Graph);
assert_impl_all!(LongStrangle: Strategable, Graph);
assert_impl_all!(ShortStrangle: Strategable, Graph);
assert_impl_all!(LongCall: Strategable, Graph);
assert_impl_all!(LongPut: Strategable, Graph);
assert_impl_all!(ShortCall: Strategable, Graph);
assert_impl_all!(ShortPut: Strategable, Graph);
assert_impl_all!(PoorMansCoveredCall: Strategable, Graph);
assert_impl_all!(BullCallLadder: Strategable, Graph);
assert_impl_all!(CustomStrategy: Strategable, Graph);
assert_impl_all!(CoveredCall: Strategable, Graph);
assert_impl_all!(Collar: Strategable, Graph);
assert_impl_all!(ProtectivePut: Strategable, Graph);
}
}