Module: OptionLab::Plotting

Defined in:
lib/option_lab/plotting.rb

Class Method Summary collapse

Class Method Details

.find_break_even_points(prices, profits) ⇒ Array<Float> (private)

Find approximate break-even points where profit/loss crosses zero

Parameters:

  • prices (Array<Float>)

    Array of stock prices

  • profits (Array<Float>)

    Array of profit/loss values

Returns:

  • (Array<Float>)

    Approximate break-even points



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/option_lab/plotting.rb', line 160

def find_break_even_points(prices, profits)
  break_even_points = []

  # Find where profit crosses zero (sign changes)
  (0...profits.size - 1).each do |i|
    if (profits[i] <= 0 && profits[i + 1] > 0) || (profits[i] >= 0 && profits[i + 1] < 0)
      # Linear interpolation to find more accurate break-even point
      if profits[i] != profits[i + 1] # Avoid division by zero
        ratio = profits[i].abs / (profits[i].abs + profits[i + 1].abs)
        break_even = prices[i] + ratio * (prices[i + 1] - prices[i])
        break_even_points << break_even.round(2)
      else
        # If same profit (unlikely but possible), use midpoint
        break_even_points << ((prices[i] + prices[i + 1]) / 2).round(2)
      end
    end
  end

  break_even_points
end

.plot_pl(outputs) ⇒ void

This method returns an undefined value.

Plot profit/loss diagram

Parameters:



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/option_lab/plotting.rb', line 11

def plot_pl(outputs)
  st = outputs.data
  inputs = outputs.inputs

  if st.strategy_profit.empty?
    raise RuntimeError, 'Before plotting the profit/loss profile diagram, you must run a calculation!'
  end

  # Extract data
  stock_prices = st.stock_price_array
  strategy_profit = st.strategy_profit

  # Print explanation for the plot
  comment = "Profit/Loss diagram:\n--------------------\n"
  comment += "The vertical line (|) corresponds to the stock's current price (#{inputs.stock_price}).\n"
  comment += "Break-even points are where the line crosses zero.\n"

  # Process strikes and add to comment
  call_buy_strikes = []
  put_buy_strikes = []
  call_sell_strikes = []
  put_sell_strikes = []

  st.strike.each_with_index do |strike, i|
    next if strike == 0.0

    case st.type[i]
    when 'call'
      if st.action[i] == 'buy'
        call_buy_strikes << strike
      elsif st.action[i] == 'sell'
        call_sell_strikes << strike
      end
    when 'put'
      if st.action[i] == 'buy'
        put_buy_strikes << strike
      elsif st.action[i] == 'sell'
        put_sell_strikes << strike
      end
    end
  end

  if call_buy_strikes.any?
    comment += "Long Call Strikes: #{call_buy_strikes.join(', ')}\n"
  end

  if call_sell_strikes.any?
    comment += "Short Call Strikes: #{call_sell_strikes.join(', ')}\n"
  end

  if put_buy_strikes.any?
    comment += "Long Put Strikes: #{put_buy_strikes.join(', ')}\n"
  end

  if put_sell_strikes.any?
    comment += "Short Put Strikes: #{put_sell_strikes.join(', ')}\n"
  end

  # Handle profit target and loss limit
  if inputs.profit_target
    comment += "Profit Target: $#{inputs.profit_target}\n"
  end

  if inputs.loss_limit
    comment += "Loss Limit: $#{inputs.loss_limit}\n"
  end

  # Print comment
  puts comment

  # Create LineChart with stock prices and strategy profit
  plot = UnicodePlot.lineplot(
    stock_prices.to_a,
    strategy_profit.to_a,
    title: 'Options Strategy Profit/Loss',
    xlabel: 'Stock Price',
    ylabel: 'Profit/Loss'
  )

  # Add horizontal zero line for break-even
  zero_line = Array.new(stock_prices.size, 0)
  plot = UnicodePlot.lineplot!(
    plot,
    stock_prices.to_a,
    zero_line,
    name: 'Break-even',
    color: :magenta
  )

  # Add vertical line at current stock price
  # Find index closest to current stock price
  current_price_idx = stock_prices.to_a.index { |p| p >= inputs.stock_price } || (stock_prices.size / 2)
  current_x = [stock_prices[current_price_idx], stock_prices[current_price_idx]]
  current_y = [strategy_profit.min, strategy_profit.max]

  plot = UnicodePlot.lineplot!(
    plot,
    current_x,
    current_y,
    name: 'Current Price',
    color: :green
  )

  # Add profit target line if specified
  if inputs.profit_target
    target_line = Array.new(stock_prices.size, inputs.profit_target)
    plot = UnicodePlot.lineplot!(
      plot,
      stock_prices.to_a,
      target_line,
      name: 'Profit Target',
      color: :blue
    )
  end

  # Add loss limit line if specified
  if inputs.loss_limit
    loss_line = Array.new(stock_prices.size, inputs.loss_limit)
    plot = UnicodePlot.lineplot!(
      plot,
      stock_prices.to_a,
      loss_line,
      name: 'Loss Limit',
      color: :red
    )
  end

  # Display the plot
  puts plot

  # Print break-even points
  break_even_points = find_break_even_points(stock_prices.to_a, strategy_profit.to_a)
  if break_even_points.any?
    puts "\nBreak-even prices: #{break_even_points.map { |p| sprintf('$%.2f', p) }.join(', ')}"
  else
    puts "\nNo break-even points found in the analyzed price range."
  end

  # Print max profit/loss in range
  puts "Maximum profit in range: $#{strategy_profit.max.round(2)}"
  puts "Maximum loss in range: $#{strategy_profit.min.round(2)}"
end