option_lab 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +139 -0
- data/.yard/hooks/before_generate.rb +7 -0
- data/.yardopts +11 -0
- data/Gemfile +26 -0
- data/LICENSE.txt +21 -0
- data/README.md +180 -0
- data/Rakefile +44 -0
- data/docs/OptionLab/BinomialTree.html +1271 -0
- data/docs/OptionLab/BjerksundStensland.html +2022 -0
- data/docs/OptionLab/BlackScholes.html +2388 -0
- data/docs/OptionLab/Engine.html +1716 -0
- data/docs/OptionLab/Models/AmericanModelInputs.html +937 -0
- data/docs/OptionLab/Models/ArrayInputs.html +463 -0
- data/docs/OptionLab/Models/BaseModel.html +223 -0
- data/docs/OptionLab/Models/BinomialModelInputs.html +1161 -0
- data/docs/OptionLab/Models/BlackScholesInfo.html +967 -0
- data/docs/OptionLab/Models/BlackScholesModelInputs.html +851 -0
- data/docs/OptionLab/Models/ClosedPosition.html +445 -0
- data/docs/OptionLab/Models/EngineData.html +2523 -0
- data/docs/OptionLab/Models/EngineDataResults.html +435 -0
- data/docs/OptionLab/Models/Inputs.html +2241 -0
- data/docs/OptionLab/Models/LaplaceInputs.html +777 -0
- data/docs/OptionLab/Models/Option.html +736 -0
- data/docs/OptionLab/Models/Outputs.html +1753 -0
- data/docs/OptionLab/Models/PoPOutputs.html +645 -0
- data/docs/OptionLab/Models/PricingResult.html +848 -0
- data/docs/OptionLab/Models/Stock.html +583 -0
- data/docs/OptionLab/Models/TreeVisualization.html +688 -0
- data/docs/OptionLab/Models.html +251 -0
- data/docs/OptionLab/Plotting.html +548 -0
- data/docs/OptionLab/Support.html +2884 -0
- data/docs/OptionLab/Utils.html +619 -0
- data/docs/OptionLab.html +133 -0
- data/docs/_index.html +376 -0
- data/docs/class_list.html +54 -0
- data/docs/css/common.css +1 -0
- data/docs/css/full_list.css +58 -0
- data/docs/css/style.css +503 -0
- data/docs/file.LICENSE.html +70 -0
- data/docs/file.README.html +263 -0
- data/docs/file_list.html +64 -0
- data/docs/frames.html +22 -0
- data/docs/index.html +263 -0
- data/docs/js/app.js +344 -0
- data/docs/js/full_list.js +242 -0
- data/docs/js/jquery.js +4 -0
- data/docs/method_list.html +1974 -0
- data/docs/top-level-namespace.html +110 -0
- data/examples/american_options.rb +163 -0
- data/examples/covered_call.rb +76 -0
- data/lib/option_lab/binomial_tree.rb +238 -0
- data/lib/option_lab/bjerksund_stensland.rb +276 -0
- data/lib/option_lab/black_scholes.rb +323 -0
- data/lib/option_lab/engine.rb +492 -0
- data/lib/option_lab/models.rb +768 -0
- data/lib/option_lab/plotting.rb +182 -0
- data/lib/option_lab/support.rb +471 -0
- data/lib/option_lab/utils.rb +107 -0
- data/lib/option_lab/version.rb +5 -0
- data/lib/option_lab.rb +134 -0
- data/option_lab.gemspec +43 -0
- metadata +207 -0
@@ -0,0 +1,110 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8">
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6
|
+
<title>
|
7
|
+
Top Level Namespace
|
8
|
+
|
9
|
+
— OptionLab Documentation
|
10
|
+
|
11
|
+
</title>
|
12
|
+
|
13
|
+
<link rel="stylesheet" href="css/style.css" type="text/css" />
|
14
|
+
|
15
|
+
<link rel="stylesheet" href="css/common.css" type="text/css" />
|
16
|
+
|
17
|
+
<script type="text/javascript">
|
18
|
+
pathId = "";
|
19
|
+
relpath = '';
|
20
|
+
</script>
|
21
|
+
|
22
|
+
|
23
|
+
<script type="text/javascript" charset="utf-8" src="js/jquery.js"></script>
|
24
|
+
|
25
|
+
<script type="text/javascript" charset="utf-8" src="js/app.js"></script>
|
26
|
+
|
27
|
+
|
28
|
+
</head>
|
29
|
+
<body>
|
30
|
+
<div class="nav_wrap">
|
31
|
+
<iframe id="nav" src="class_list.html?1"></iframe>
|
32
|
+
<div id="resizer"></div>
|
33
|
+
</div>
|
34
|
+
|
35
|
+
<div id="main" tabindex="-1">
|
36
|
+
<div id="header">
|
37
|
+
<div id="menu">
|
38
|
+
|
39
|
+
<a href="_index.html">Index</a> »
|
40
|
+
|
41
|
+
|
42
|
+
<span class="title">Top Level Namespace</span>
|
43
|
+
|
44
|
+
</div>
|
45
|
+
|
46
|
+
<div id="search">
|
47
|
+
|
48
|
+
<a class="full_list_link" id="class_list_link"
|
49
|
+
href="class_list.html">
|
50
|
+
|
51
|
+
<svg width="24" height="24">
|
52
|
+
<rect x="0" y="4" width="24" height="4" rx="1" ry="1"></rect>
|
53
|
+
<rect x="0" y="12" width="24" height="4" rx="1" ry="1"></rect>
|
54
|
+
<rect x="0" y="20" width="24" height="4" rx="1" ry="1"></rect>
|
55
|
+
</svg>
|
56
|
+
</a>
|
57
|
+
|
58
|
+
</div>
|
59
|
+
<div class="clear"></div>
|
60
|
+
</div>
|
61
|
+
|
62
|
+
<div id="content"><h1>Top Level Namespace
|
63
|
+
|
64
|
+
|
65
|
+
|
66
|
+
</h1>
|
67
|
+
<div class="box_info">
|
68
|
+
|
69
|
+
|
70
|
+
|
71
|
+
|
72
|
+
|
73
|
+
|
74
|
+
|
75
|
+
|
76
|
+
|
77
|
+
|
78
|
+
|
79
|
+
</div>
|
80
|
+
|
81
|
+
<h2>Defined Under Namespace</h2>
|
82
|
+
<p class="children">
|
83
|
+
|
84
|
+
|
85
|
+
<strong class="modules">Modules:</strong> <span class='object_link'><a href="OptionLab.html" title="OptionLab (module)">OptionLab</a></span>
|
86
|
+
|
87
|
+
|
88
|
+
|
89
|
+
|
90
|
+
</p>
|
91
|
+
|
92
|
+
|
93
|
+
|
94
|
+
|
95
|
+
|
96
|
+
|
97
|
+
|
98
|
+
|
99
|
+
|
100
|
+
</div>
|
101
|
+
|
102
|
+
<div id="footer">
|
103
|
+
Generated on Sun Apr 27 16:09:33 2025 by
|
104
|
+
<a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
|
105
|
+
0.9.37 (ruby-3.3.3).
|
106
|
+
</div>
|
107
|
+
|
108
|
+
</div>
|
109
|
+
</body>
|
110
|
+
</html>
|
@@ -0,0 +1,163 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
# Use the local code instead of the installed gem
|
5
|
+
require_relative '../lib/option_lab'
|
6
|
+
|
7
|
+
# Example of pricing American options using different models
|
8
|
+
puts 'American Option Pricing Models Comparison'
|
9
|
+
puts '----------------------------------------'
|
10
|
+
|
11
|
+
# Input parameters
|
12
|
+
spot_price = 100.0
|
13
|
+
strike_price = 105.0
|
14
|
+
risk_free_rate = 0.05
|
15
|
+
volatility = 0.25
|
16
|
+
dividend_yield = 0.03
|
17
|
+
time_to_maturity = 1.0 # 1 year
|
18
|
+
|
19
|
+
# Calculate option prices using both models
|
20
|
+
puts "\nCall Option Comparison:"
|
21
|
+
puts '----------------------'
|
22
|
+
|
23
|
+
# Black-Scholes (European)
|
24
|
+
bs_call_price = OptionLab::BlackScholes.get_bs_info(
|
25
|
+
spot_price, strike_price, risk_free_rate, volatility, time_to_maturity, dividend_yield
|
26
|
+
).call_price
|
27
|
+
|
28
|
+
puts "European Call (Black-Scholes): $#{bs_call_price.round(4)}"
|
29
|
+
|
30
|
+
# Cox-Ross-Rubinstein Binomial Tree (American)
|
31
|
+
crr_call_price = OptionLab.price_binomial(
|
32
|
+
'call', spot_price, strike_price, risk_free_rate, volatility, time_to_maturity, 100, true, dividend_yield
|
33
|
+
)
|
34
|
+
|
35
|
+
puts "American Call (CRR Binomial): $#{crr_call_price.round(4)}"
|
36
|
+
|
37
|
+
# Bjerksund-Stensland (American)
|
38
|
+
bs_am_call_price = OptionLab.price_american(
|
39
|
+
'call', spot_price, strike_price, risk_free_rate, volatility, time_to_maturity, dividend_yield
|
40
|
+
)
|
41
|
+
|
42
|
+
puts "American Call (Bjerksund-Stensland): $#{bs_am_call_price.round(4)}"
|
43
|
+
|
44
|
+
# Put option prices
|
45
|
+
puts "\nPut Option Comparison:"
|
46
|
+
puts '---------------------'
|
47
|
+
|
48
|
+
# Black-Scholes (European)
|
49
|
+
bs_put_price = OptionLab::BlackScholes.get_bs_info(
|
50
|
+
spot_price, strike_price, risk_free_rate, volatility, time_to_maturity, dividend_yield
|
51
|
+
).put_price
|
52
|
+
|
53
|
+
puts "European Put (Black-Scholes): $#{bs_put_price.round(4)}"
|
54
|
+
|
55
|
+
# Cox-Ross-Rubinstein Binomial Tree (American)
|
56
|
+
crr_put_price = OptionLab.price_binomial(
|
57
|
+
'put', spot_price, strike_price, risk_free_rate, volatility, time_to_maturity, 100, true, dividend_yield
|
58
|
+
)
|
59
|
+
|
60
|
+
puts "American Put (CRR Binomial): $#{crr_put_price.round(4)}"
|
61
|
+
|
62
|
+
# Bjerksund-Stensland (American)
|
63
|
+
bs_am_put_price = OptionLab.price_american(
|
64
|
+
'put', spot_price, strike_price, risk_free_rate, volatility, time_to_maturity, dividend_yield
|
65
|
+
)
|
66
|
+
|
67
|
+
puts "American Put (Bjerksund-Stensland): $#{bs_am_put_price.round(4)}"
|
68
|
+
|
69
|
+
# Calculate Greeks for the American put option (which typically has higher early exercise value)
|
70
|
+
puts "\nAmerican Put Option Greeks Comparison:"
|
71
|
+
puts '-----------------------------------'
|
72
|
+
|
73
|
+
# CRR Binomial Greeks
|
74
|
+
crr_greeks = OptionLab.get_binomial_greeks(
|
75
|
+
'put', spot_price, strike_price, risk_free_rate, volatility, time_to_maturity, 100, true, dividend_yield
|
76
|
+
)
|
77
|
+
|
78
|
+
puts 'CRR Binomial Greeks:'
|
79
|
+
puts "Delta: #{crr_greeks[:delta].round(6)}"
|
80
|
+
puts "Gamma: #{crr_greeks[:gamma].round(6)}"
|
81
|
+
puts "Theta: #{crr_greeks[:theta].round(6)}"
|
82
|
+
puts "Vega: #{crr_greeks[:vega].round(6)}"
|
83
|
+
puts "Rho: #{crr_greeks[:rho].round(6)}"
|
84
|
+
|
85
|
+
# Bjerksund-Stensland Greeks
|
86
|
+
bs_am_greeks = OptionLab.get_american_greeks(
|
87
|
+
'put', spot_price, strike_price, risk_free_rate, volatility, time_to_maturity, dividend_yield
|
88
|
+
)
|
89
|
+
|
90
|
+
puts "\nBjerksund-Stensland Greeks:"
|
91
|
+
puts "Delta: #{bs_am_greeks[:delta].round(6)}"
|
92
|
+
puts "Gamma: #{bs_am_greeks[:gamma].round(6)}"
|
93
|
+
puts "Theta: #{bs_am_greeks[:theta].round(6)}"
|
94
|
+
puts "Vega: #{bs_am_greeks[:vega].round(6)}"
|
95
|
+
puts "Rho: #{bs_am_greeks[:rho].round(6)}"
|
96
|
+
|
97
|
+
# Generate binomial tree for visualization (using smaller number of steps)
|
98
|
+
tree_data = OptionLab.get_binomial_tree(
|
99
|
+
'put', spot_price, strike_price, risk_free_rate, volatility, time_to_maturity, 5, true, dividend_yield
|
100
|
+
)
|
101
|
+
|
102
|
+
puts "\nBinomial Tree Visualization (5 steps):"
|
103
|
+
puts '-------------------------------------'
|
104
|
+
|
105
|
+
# Display tree parameters
|
106
|
+
puts 'Parameters:'
|
107
|
+
puts "Spot Price: $#{tree_data[:parameters][:spot_price]}"
|
108
|
+
puts "Strike Price: $#{tree_data[:parameters][:strike_price]}"
|
109
|
+
puts "Up Factor: #{tree_data[:parameters][:up_factor].round(4)}"
|
110
|
+
puts "Down Factor: #{tree_data[:parameters][:down_factor].round(4)}"
|
111
|
+
puts "Risk-Neutral Probability: #{tree_data[:parameters][:risk_neutral_probability].round(4)}"
|
112
|
+
|
113
|
+
puts "\nPrice Tree:"
|
114
|
+
tree_data[:stock_prices].each_with_index do |step_prices, step|
|
115
|
+
puts "Step #{step}: #{step_prices.take(step + 1).map { |p| p.round(2) }.join(', ')}"
|
116
|
+
end
|
117
|
+
|
118
|
+
puts "\nOption Value Tree:"
|
119
|
+
tree_data[:option_values].each_with_index do |step_values, step|
|
120
|
+
puts "Step #{step}: #{step_values.take(step + 1).map { |v| v.round(2) }.join(', ')}"
|
121
|
+
end
|
122
|
+
|
123
|
+
puts "\nEarly Exercise Flags:"
|
124
|
+
tree_data[:exercise_flags].each_with_index do |step_flags, step|
|
125
|
+
puts "Step #{step}: #{step_flags.take(step + 1).join(', ')}"
|
126
|
+
end
|
127
|
+
|
128
|
+
# Early exercise boundary analysis
|
129
|
+
puts "\nEarly Exercise Boundary Analysis:"
|
130
|
+
puts '--------------------------------'
|
131
|
+
|
132
|
+
# Test a range of spot prices to find the early exercise boundary
|
133
|
+
puts 'Finding early exercise boundary for American put option at various times to maturity:'
|
134
|
+
puts "Time\tBoundary"
|
135
|
+
|
136
|
+
[0.25, 0.5, 0.75, 1.0].each do |t|
|
137
|
+
# Binary search to find boundary
|
138
|
+
low = 50.0
|
139
|
+
high = strike_price
|
140
|
+
tolerance = 0.01
|
141
|
+
|
142
|
+
while (high - low) > tolerance
|
143
|
+
mid = (low + high) / 2
|
144
|
+
|
145
|
+
# Test if this spot price should be exercised early
|
146
|
+
tree = OptionLab.get_binomial_tree(
|
147
|
+
'put', mid, strike_price, risk_free_rate, volatility, t, 25, true, dividend_yield
|
148
|
+
)
|
149
|
+
|
150
|
+
if tree[:exercise_flags][0][0]
|
151
|
+
# Early exercise is optimal, look higher
|
152
|
+
low = mid
|
153
|
+
else
|
154
|
+
# Early exercise not optimal, look lower
|
155
|
+
high = mid
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
boundary = (low + high) / 2
|
160
|
+
puts "#{t}\t$#{boundary.round(2)}"
|
161
|
+
end
|
162
|
+
|
163
|
+
puts "\nExample completed successfully."
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
# Use the local code instead of the installed gem
|
5
|
+
require_relative '../lib/option_lab'
|
6
|
+
|
7
|
+
# Example of a covered call strategy
|
8
|
+
# A covered call involves owning the underlying stock and selling call options against it
|
9
|
+
|
10
|
+
# Step 1: Define input parameters
|
11
|
+
input_data = {
|
12
|
+
stock_price: 168.99, # Current stock price
|
13
|
+
volatility: 0.483, # Annualized volatility (48.3%)
|
14
|
+
interest_rate: 0.045, # Annualized risk-free interest rate (4.5%)
|
15
|
+
start_date: Date.new(2023, 1, 16), # Strategy start date
|
16
|
+
target_date: Date.new(2023, 2, 17), # Strategy target date
|
17
|
+
min_stock: 68.99, # Minimum stock price for analysis
|
18
|
+
max_stock: 268.99, # Maximum stock price for analysis
|
19
|
+
|
20
|
+
# The covered call strategy is defined with two legs:
|
21
|
+
# 1. Long 100 shares of stock
|
22
|
+
# 2. Short 1 call option with strike price 185.0 and premium 4.1
|
23
|
+
strategy: [
|
24
|
+
{ type: 'stock', n: 100, action: 'buy' },
|
25
|
+
{
|
26
|
+
type: 'call',
|
27
|
+
strike: 185.0,
|
28
|
+
premium: 4.1,
|
29
|
+
n: 100,
|
30
|
+
action: 'sell',
|
31
|
+
expiration: Date.new(2023, 2, 17),
|
32
|
+
},
|
33
|
+
],
|
34
|
+
}
|
35
|
+
|
36
|
+
# Step 2: Run the strategy calculation
|
37
|
+
puts 'Calculating strategy outcomes...'
|
38
|
+
outputs = OptionLab.run_strategy(input_data)
|
39
|
+
|
40
|
+
# Step 3: Print the results
|
41
|
+
puts "\nCovered Call Strategy Results:"
|
42
|
+
puts '------------------------------'
|
43
|
+
puts outputs
|
44
|
+
|
45
|
+
# Step 4: Extract and display key metrics
|
46
|
+
puts "\nKey Metrics:"
|
47
|
+
puts "Probability of Profit: #{(outputs.probability_of_profit * 100).round(2)}%"
|
48
|
+
puts "Expected Profit (when profitable): $#{outputs.expected_profit}" if outputs.expected_profit
|
49
|
+
puts "Expected Loss (when unprofitable): $#{outputs.expected_loss}" if outputs.expected_loss
|
50
|
+
puts "Strategy Cost: $#{outputs.strategy_cost}"
|
51
|
+
puts "Maximum Return: $#{outputs.maximum_return_in_the_domain}"
|
52
|
+
puts "Minimum Return: $#{outputs.minimum_return_in_the_domain}"
|
53
|
+
|
54
|
+
# Step 5: Print Greeks for each leg
|
55
|
+
puts "\nGreeks by Strategy Leg:"
|
56
|
+
puts 'Leg 1 (Stock):'
|
57
|
+
puts " Delta: #{outputs.delta[0]}"
|
58
|
+
|
59
|
+
puts 'Leg 2 (Call Option):'
|
60
|
+
puts " Delta: #{outputs.delta[1]}"
|
61
|
+
puts " Gamma: #{outputs.gamma[1]}"
|
62
|
+
puts " Theta: #{outputs.theta[1]}"
|
63
|
+
puts " Vega: #{outputs.vega[1]}"
|
64
|
+
puts " Rho: #{outputs.rho[1]}"
|
65
|
+
puts " Implied Volatility: #{(outputs.implied_volatility[1] * 100).round(2)}%"
|
66
|
+
puts " ITM Probability: #{(outputs.in_the_money_probability[1] * 100).round(2)}%"
|
67
|
+
|
68
|
+
# Step 6: Export P/L data to CSV
|
69
|
+
csv_filename = 'covered_call_pl.csv'
|
70
|
+
OptionLab.pl_to_csv(outputs, csv_filename)
|
71
|
+
puts "\nProfit/Loss data exported to #{csv_filename}"
|
72
|
+
|
73
|
+
# Step 7: Plot the profit/loss diagram
|
74
|
+
puts "\nGenerating profit/loss diagram..."
|
75
|
+
OptionLab.plot_pl(outputs)
|
76
|
+
puts 'Plot displayed. Close the plot window to exit.'
|
@@ -0,0 +1,238 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'numo/narray'
|
4
|
+
|
5
|
+
module OptionLab
|
6
|
+
|
7
|
+
# Implementation of the Cox-Ross-Rubinstein (CRR) binomial tree model
|
8
|
+
# for American and European options pricing
|
9
|
+
module BinomialTree
|
10
|
+
|
11
|
+
class << self
|
12
|
+
|
13
|
+
# Price an option using the Cox-Ross-Rubinstein binomial tree model
|
14
|
+
# @param option_type [String] 'call' or 'put'
|
15
|
+
# @param s0 [Float] Spot price
|
16
|
+
# @param x [Float] Strike price
|
17
|
+
# @param r [Float] Risk-free interest rate
|
18
|
+
# @param volatility [Float] Volatility
|
19
|
+
# @param years_to_maturity [Float] Time to maturity in years
|
20
|
+
# @param n_steps [Integer] Number of time steps
|
21
|
+
# @param is_american [Boolean] True for American options, false for European
|
22
|
+
# @param dividend_yield [Float] Continuous dividend yield
|
23
|
+
# @return [Float] Option price
|
24
|
+
def price_option(option_type, s0, x, r, volatility, years_to_maturity, n_steps = 100, is_american = true, dividend_yield = 0.0)
|
25
|
+
# Calculate time step
|
26
|
+
dt = years_to_maturity / n_steps.to_f
|
27
|
+
|
28
|
+
# Calculate up and down factors
|
29
|
+
u = Math.exp(volatility * Math.sqrt(dt))
|
30
|
+
d = 1.0 / u
|
31
|
+
|
32
|
+
# Calculate risk-neutral probability
|
33
|
+
effective_r = r - dividend_yield
|
34
|
+
p = (Math.exp(effective_r * dt) - d) / (u - d)
|
35
|
+
|
36
|
+
# Calculate discount factor
|
37
|
+
discount = Math.exp(-r * dt)
|
38
|
+
|
39
|
+
# Initialize price tree
|
40
|
+
stock_prices = Array.new(n_steps + 1) { Array.new(n_steps + 1, 0.0) }
|
41
|
+
option_values = Array.new(n_steps + 1) { Array.new(n_steps + 1, 0.0) }
|
42
|
+
|
43
|
+
# Fill stock price tree
|
44
|
+
(0..n_steps).each do |i|
|
45
|
+
(0..i).each do |j|
|
46
|
+
stock_prices[i][j] = s0 * (u**(i - j)) * (d**j)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Calculate option values at expiration (i = n_steps)
|
51
|
+
(0..n_steps).each do |j|
|
52
|
+
option_values[n_steps][j] = option_payoff(option_type, stock_prices[n_steps][j], x)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Work backwards through the tree
|
56
|
+
(n_steps - 1).downto(0) do |i|
|
57
|
+
(0..i).each do |j|
|
58
|
+
# Expected value (European option value)
|
59
|
+
expected_value = discount * (p * option_values[i + 1][j] + (1 - p) * option_values[i + 1][j + 1])
|
60
|
+
|
61
|
+
if is_american
|
62
|
+
# For American options, compare with immediate exercise value
|
63
|
+
exercise_value = option_payoff(option_type, stock_prices[i][j], x)
|
64
|
+
option_values[i][j] = [expected_value, exercise_value].max
|
65
|
+
else
|
66
|
+
# For European options, use expected value
|
67
|
+
option_values[i][j] = expected_value
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Root node contains the option price
|
73
|
+
option_values[0][0]
|
74
|
+
end
|
75
|
+
|
76
|
+
# Calculate option payoff at expiration
|
77
|
+
# @param option_type [String] 'call' or 'put'
|
78
|
+
# @param stock_price [Float] Stock price
|
79
|
+
# @param strike [Float] Strike price
|
80
|
+
# @return [Float] Option payoff
|
81
|
+
def option_payoff(option_type, stock_price, strike)
|
82
|
+
if option_type == 'call'
|
83
|
+
[stock_price - strike, 0.0].max
|
84
|
+
elsif option_type == 'put'
|
85
|
+
[strike - stock_price, 0.0].max
|
86
|
+
else
|
87
|
+
raise ArgumentError, "Option type must be either 'call' or 'put'!"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Get a full binomial tree as a structured output
|
92
|
+
# @param option_type [String] 'call' or 'put'
|
93
|
+
# @param s0 [Float] Spot price
|
94
|
+
# @param x [Float] Strike price
|
95
|
+
# @param r [Float] Risk-free interest rate
|
96
|
+
# @param volatility [Float] Volatility
|
97
|
+
# @param years_to_maturity [Float] Time to maturity in years
|
98
|
+
# @param n_steps [Integer] Number of time steps
|
99
|
+
# @param is_american [Boolean] True for American options, false for European
|
100
|
+
# @param dividend_yield [Float] Continuous dividend yield
|
101
|
+
# @return [Hash] Tree structure with stock prices and option values
|
102
|
+
def get_tree(option_type, s0, x, r, volatility, years_to_maturity, n_steps = 15, is_american = true, dividend_yield = 0.0)
|
103
|
+
# Calculate time step
|
104
|
+
dt = years_to_maturity / n_steps.to_f
|
105
|
+
|
106
|
+
# Calculate up and down factors
|
107
|
+
u = Math.exp(volatility * Math.sqrt(dt))
|
108
|
+
d = 1.0 / u
|
109
|
+
|
110
|
+
# Calculate risk-neutral probability
|
111
|
+
effective_r = r - dividend_yield
|
112
|
+
p = (Math.exp(effective_r * dt) - d) / (u - d)
|
113
|
+
|
114
|
+
# Calculate discount factor
|
115
|
+
discount = Math.exp(-r * dt)
|
116
|
+
|
117
|
+
# Initialize price tree
|
118
|
+
stock_prices = Array.new(n_steps + 1) { Array.new(n_steps + 1, 0.0) }
|
119
|
+
option_values = Array.new(n_steps + 1) { Array.new(n_steps + 1, 0.0) }
|
120
|
+
exercise_flags = Array.new(n_steps + 1) { Array.new(n_steps + 1, false) }
|
121
|
+
|
122
|
+
# Fill stock price tree
|
123
|
+
(0..n_steps).each do |i|
|
124
|
+
(0..i).each do |j|
|
125
|
+
stock_prices[i][j] = s0 * (u**(i - j)) * (d**j)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Calculate option values at expiration (i = n_steps)
|
130
|
+
(0..n_steps).each do |j|
|
131
|
+
option_values[n_steps][j] = option_payoff(option_type, stock_prices[n_steps][j], x)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Work backwards through the tree
|
135
|
+
(n_steps - 1).downto(0) do |i|
|
136
|
+
(0..i).each do |j|
|
137
|
+
# Expected value (European option value)
|
138
|
+
expected_value = discount * (p * option_values[i + 1][j] + (1 - p) * option_values[i + 1][j + 1])
|
139
|
+
|
140
|
+
if is_american
|
141
|
+
# For American options, compare with immediate exercise value
|
142
|
+
exercise_value = option_payoff(option_type, stock_prices[i][j], x)
|
143
|
+
|
144
|
+
if exercise_value > expected_value
|
145
|
+
option_values[i][j] = exercise_value
|
146
|
+
exercise_flags[i][j] = true
|
147
|
+
else
|
148
|
+
option_values[i][j] = expected_value
|
149
|
+
end
|
150
|
+
else
|
151
|
+
# For European options, use expected value
|
152
|
+
option_values[i][j] = expected_value
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Return tree structure
|
158
|
+
{
|
159
|
+
stock_prices: stock_prices,
|
160
|
+
option_values: option_values,
|
161
|
+
exercise_flags: exercise_flags,
|
162
|
+
parameters: {
|
163
|
+
option_type: option_type,
|
164
|
+
spot_price: s0,
|
165
|
+
strike_price: x,
|
166
|
+
risk_free_rate: r,
|
167
|
+
volatility: volatility,
|
168
|
+
time_to_maturity: years_to_maturity,
|
169
|
+
steps: n_steps,
|
170
|
+
is_american: is_american,
|
171
|
+
dividend_yield: dividend_yield,
|
172
|
+
up_factor: u,
|
173
|
+
down_factor: d,
|
174
|
+
risk_neutral_probability: p,
|
175
|
+
},
|
176
|
+
}
|
177
|
+
end
|
178
|
+
|
179
|
+
# Calculate option Greeks using the CRR model and finite difference methods
|
180
|
+
# @param option_type [String] 'call' or 'put'
|
181
|
+
# @param s0 [Float] Spot price
|
182
|
+
# @param x [Float] Strike price
|
183
|
+
# @param r [Float] Risk-free interest rate
|
184
|
+
# @param volatility [Float] Volatility
|
185
|
+
# @param years_to_maturity [Float] Time to maturity in years
|
186
|
+
# @param n_steps [Integer] Number of time steps
|
187
|
+
# @param is_american [Boolean] True for American options, false for European
|
188
|
+
# @param dividend_yield [Float] Continuous dividend yield
|
189
|
+
# @return [Hash] Option Greeks (delta, gamma, theta, vega, rho)
|
190
|
+
def get_greeks(option_type, s0, x, r, volatility, years_to_maturity, n_steps = 100, is_american = true, dividend_yield = 0.0)
|
191
|
+
# Small increment for finite difference calculation
|
192
|
+
h_s = s0 * 0.001 # For Delta and Gamma
|
193
|
+
h_t = 1.0 / 365 # For Theta (1 day)
|
194
|
+
h_v = 0.001 # For Vega
|
195
|
+
h_r = 0.0001 # For Rho
|
196
|
+
|
197
|
+
# Base price
|
198
|
+
price = price_option(option_type, s0, x, r, volatility, years_to_maturity, n_steps, is_american, dividend_yield)
|
199
|
+
|
200
|
+
# Delta: ∂V/∂S
|
201
|
+
price_up = price_option(option_type, s0 + h_s, x, r, volatility, years_to_maturity, n_steps, is_american, dividend_yield)
|
202
|
+
price_down = price_option(option_type, s0 - h_s, x, r, volatility, years_to_maturity, n_steps, is_american, dividend_yield)
|
203
|
+
delta = (price_up - price_down) / (2 * h_s)
|
204
|
+
|
205
|
+
# Gamma: ∂²V/∂S²
|
206
|
+
gamma = (price_up - 2 * price + price_down) / (h_s * h_s)
|
207
|
+
|
208
|
+
# Theta: -∂V/∂t
|
209
|
+
price_t_down = if years_to_maturity - h_t > 0
|
210
|
+
price_option(option_type, s0, x, r, volatility, years_to_maturity - h_t, n_steps, is_american, dividend_yield)
|
211
|
+
else
|
212
|
+
option_payoff(option_type, s0, x)
|
213
|
+
end
|
214
|
+
theta = -(price_t_down - price) / h_t
|
215
|
+
|
216
|
+
# Vega: ∂V/∂σ
|
217
|
+
price_v_up = price_option(option_type, s0, x, r, volatility + h_v, years_to_maturity, n_steps, is_american, dividend_yield)
|
218
|
+
vega = (price_v_up - price) / h_v
|
219
|
+
|
220
|
+
# Rho: ∂V/∂r
|
221
|
+
price_r_up = price_option(option_type, s0, x, r + h_r, volatility, years_to_maturity, n_steps, is_american, dividend_yield)
|
222
|
+
rho = (price_r_up - price) / h_r
|
223
|
+
|
224
|
+
# Return Greeks
|
225
|
+
{
|
226
|
+
delta: delta,
|
227
|
+
gamma: gamma,
|
228
|
+
theta: theta,
|
229
|
+
vega: vega,
|
230
|
+
rho: rho,
|
231
|
+
}
|
232
|
+
end
|
233
|
+
|
234
|
+
end
|
235
|
+
|
236
|
+
end
|
237
|
+
|
238
|
+
end
|