penfold 1.0.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.
- data/.autotest +23 -0
- data/History.txt +6 -0
- data/Manifest.txt +22 -0
- data/README.rdoc +167 -0
- data/Rakefile +13 -0
- data/bin/penfold +14 -0
- data/bin/penfold-position +173 -0
- data/bin/penfold-try +184 -0
- data/lib/argument_processor.rb +6 -0
- data/lib/commission.rb +44 -0
- data/lib/core_ext.rb +32 -0
- data/lib/covered_call_early_exit.rb +26 -0
- data/lib/covered_call_exit.rb +82 -0
- data/lib/covered_call_expiry_itm_exit.rb +26 -0
- data/lib/covered_call_expiry_otm_exit.rb +24 -0
- data/lib/covered_call_position.rb +86 -0
- data/lib/market.rb +254 -0
- data/lib/option.rb +96 -0
- data/lib/penfold.rb +20 -0
- data/lib/stock.rb +15 -0
- data/portfolio.yml +277 -0
- data/test/test_option_calendar.rb +74 -0
- data/test/test_penfold.rb +315 -0
- metadata +128 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
module CoveredCallEarlyExit
|
2
|
+
def proceeds
|
3
|
+
stock_sale - option_sale
|
4
|
+
end
|
5
|
+
|
6
|
+
def explain
|
7
|
+
template = <<-EOT
|
8
|
+
Early Exit
|
9
|
+
Stock Sale %s
|
10
|
+
- Call Buyback %s
|
11
|
+
= Net Proceeds %s (%s per share)
|
12
|
+
- Net Outlay %s
|
13
|
+
= Profit %s
|
14
|
+
EOT
|
15
|
+
|
16
|
+
template % [
|
17
|
+
stock_sale.to_money_s.rjust(12),
|
18
|
+
option_sale.to_money_s.rjust(12),
|
19
|
+
proceeds.to_money_s.rjust(12),
|
20
|
+
proceeds_per_share.to_money_s,
|
21
|
+
opening_position.net_outlay.to_money_s.rjust(12),
|
22
|
+
profit.to_money_s.rjust(12)
|
23
|
+
]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
@@ -0,0 +1,82 @@
|
|
1
|
+
class CoveredCallExit
|
2
|
+
include ArgumentProcessor
|
3
|
+
|
4
|
+
attr_reader :opening_position, :option
|
5
|
+
|
6
|
+
def initialize(args = {})
|
7
|
+
@opening_position = args[:opening_position]
|
8
|
+
|
9
|
+
@option = opening_position.option.dup
|
10
|
+
@option.stock = opening_position.stock.dup
|
11
|
+
|
12
|
+
stock.price = args[:stock_price] if args[:stock_price]
|
13
|
+
option.price = args[:option_price] if args[:option_price]
|
14
|
+
option.current_date = args[:exit_date] if args[:exit_date]
|
15
|
+
|
16
|
+
raise ArgumentError, "Stock does not match" unless stock == opening_position.stock
|
17
|
+
raise ArgumentError, "Option does not match" unless option == opening_position.option
|
18
|
+
|
19
|
+
extend exit_type
|
20
|
+
end
|
21
|
+
|
22
|
+
def exit_type
|
23
|
+
if option.expired? and option.in_the_money? then CoveredCallExpiryItmExit
|
24
|
+
elsif option.expired? and option.out_the_money? then CoveredCallExpiryOtmExit
|
25
|
+
else CoveredCallEarlyExit
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def days_in_position
|
30
|
+
([option.expires, option.current_date].min - opening_position.date_established).to_i
|
31
|
+
end
|
32
|
+
|
33
|
+
def annualized_return
|
34
|
+
#(1 + period_return) ** (1 / (opening_position.option.days_to_expiry/365.0)) - 1
|
35
|
+
(1 + period_return) ** (1 / (days_in_position/365.0)) - 1
|
36
|
+
end
|
37
|
+
|
38
|
+
def period_return
|
39
|
+
profit / opening_position.net_outlay.to_f
|
40
|
+
end
|
41
|
+
|
42
|
+
def profit
|
43
|
+
proceeds - opening_position.net_outlay
|
44
|
+
end
|
45
|
+
|
46
|
+
def proceeds
|
47
|
+
raise NotImplementedError
|
48
|
+
end
|
49
|
+
|
50
|
+
def proceeds_per_share
|
51
|
+
proceeds / num_shares
|
52
|
+
end
|
53
|
+
|
54
|
+
def stock
|
55
|
+
option.stock
|
56
|
+
end
|
57
|
+
|
58
|
+
def commission
|
59
|
+
opening_position.commission
|
60
|
+
end
|
61
|
+
|
62
|
+
def num_shares
|
63
|
+
opening_position.num_shares
|
64
|
+
end
|
65
|
+
|
66
|
+
def exit_date
|
67
|
+
opening_position.date_established + days_in_position
|
68
|
+
end
|
69
|
+
|
70
|
+
def exercise
|
71
|
+
(num_shares * option.strike) - commission.option_assignment
|
72
|
+
end
|
73
|
+
|
74
|
+
def stock_sale
|
75
|
+
(num_shares * stock.price) - commission.stock_entry
|
76
|
+
end
|
77
|
+
|
78
|
+
def option_sale
|
79
|
+
(num_shares * option.price) - commission.option_entry
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module CoveredCallExpiryItmExit
|
2
|
+
def proceeds
|
3
|
+
exercise
|
4
|
+
end
|
5
|
+
|
6
|
+
def explain
|
7
|
+
template = <<-EOT
|
8
|
+
Call Expires ITM
|
9
|
+
Exercise %s (inc %s Assignment Fee)
|
10
|
+
= Net Proceeds %s (%s per share)
|
11
|
+
- Net Outlay %s
|
12
|
+
= Profit %s
|
13
|
+
|
14
|
+
EOT
|
15
|
+
|
16
|
+
template % [
|
17
|
+
exercise.to_money_s.rjust(12),
|
18
|
+
commission.option_assignment.to_money_s,
|
19
|
+
proceeds.to_money_s.rjust(12),
|
20
|
+
proceeds_per_share.to_money_s,
|
21
|
+
opening_position.net_outlay.to_money_s.rjust(12),
|
22
|
+
profit.to_money_s.rjust(12)
|
23
|
+
]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module CoveredCallExpiryOtmExit
|
2
|
+
def proceeds
|
3
|
+
stock_sale
|
4
|
+
end
|
5
|
+
|
6
|
+
def explain
|
7
|
+
template = <<-EOT
|
8
|
+
Call Expires OTM
|
9
|
+
Stock Sale %s (%s per share)
|
10
|
+
- Net Outlay %s
|
11
|
+
= Profit %s
|
12
|
+
|
13
|
+
|
14
|
+
EOT
|
15
|
+
|
16
|
+
template % [
|
17
|
+
stock_sale.to_money_s.rjust(12),
|
18
|
+
proceeds_per_share.to_money_s,
|
19
|
+
opening_position.net_outlay.to_money_s.rjust(12),
|
20
|
+
profit.to_money_s.rjust(12)
|
21
|
+
]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,86 @@
|
|
1
|
+
class CoveredCallPosition
|
2
|
+
include ArgumentProcessor
|
3
|
+
|
4
|
+
attr_accessor :num_shares, :date_established, :option
|
5
|
+
|
6
|
+
def initialize(args = {})
|
7
|
+
process_args(args)
|
8
|
+
end
|
9
|
+
|
10
|
+
def commission=(name)
|
11
|
+
@commission = name.to_s
|
12
|
+
end
|
13
|
+
|
14
|
+
def commission
|
15
|
+
@commission_instance ||= Commission.const_get(@commission).new(
|
16
|
+
:shares => num_shares, :contracts => num_shares / 100
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
undef num_shares=
|
21
|
+
def num_shares=(num)
|
22
|
+
raise ArgumentError, "Shares must be assigned in lots of 100." unless num % 100 == 0
|
23
|
+
@num_shares = num
|
24
|
+
end
|
25
|
+
|
26
|
+
def stock_total
|
27
|
+
num_shares * stock.price + commission.stock_entry
|
28
|
+
end
|
29
|
+
|
30
|
+
def call_sale
|
31
|
+
num_shares * option.price - commission.option_entry
|
32
|
+
end
|
33
|
+
|
34
|
+
def net_outlay
|
35
|
+
stock_total - call_sale
|
36
|
+
end
|
37
|
+
|
38
|
+
def net_per_share
|
39
|
+
net_outlay.to_f / num_shares
|
40
|
+
end
|
41
|
+
|
42
|
+
def downside_protection
|
43
|
+
(net_per_share.to_f - stock.price) / stock.price
|
44
|
+
end
|
45
|
+
|
46
|
+
def stock
|
47
|
+
option.stock
|
48
|
+
end
|
49
|
+
|
50
|
+
def implied_volatility
|
51
|
+
@iv ||= BlackScholes.call_iv(
|
52
|
+
stock.price / 100.0,
|
53
|
+
option.strike / 100.0,
|
54
|
+
0.27, # TODO: risk-free rate
|
55
|
+
option.days_to_expiry(date_established),
|
56
|
+
option.price / 100.0
|
57
|
+
)
|
58
|
+
end
|
59
|
+
|
60
|
+
def probability_max_profit
|
61
|
+
BlackScholes.probability_above(
|
62
|
+
stock.price / 100.0,
|
63
|
+
option.strike / 100.0,
|
64
|
+
option.days_to_expiry(date_established),
|
65
|
+
implied_volatility
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
def probability_profit
|
70
|
+
BlackScholes.probability_above(
|
71
|
+
stock.price / 100.0,
|
72
|
+
net_per_share / 100.0,
|
73
|
+
option.days_to_expiry(date_established),
|
74
|
+
implied_volatility
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
# def exit(exit_date = Date.today + 1, stock_price = stock.price)
|
79
|
+
# CoveredCallExit.new(
|
80
|
+
# :opening_position => self,
|
81
|
+
# :stock_price => stock_price,
|
82
|
+
# :exit_date => exit_date
|
83
|
+
# )
|
84
|
+
# end
|
85
|
+
end
|
86
|
+
|
data/lib/market.rb
ADDED
@@ -0,0 +1,254 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'nokogiri'
|
3
|
+
require 'open-uri'
|
4
|
+
require 'date'
|
5
|
+
require 'cgi'
|
6
|
+
|
7
|
+
class Market
|
8
|
+
HEADERS = {
|
9
|
+
"Accept" => "application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5",
|
10
|
+
"Accept-Charset" => "ISO-8859-1,utf-8;q=0.7,*;q=0.3",
|
11
|
+
"Accept-Language" => "en-US,en;q=0.8",
|
12
|
+
"Cache-Control" => "max-age=0",
|
13
|
+
"Connection" => "keep-alive",
|
14
|
+
"Host" => "finance.yahoo.com",
|
15
|
+
"Referer" => "http://finance.yahoo.com/",
|
16
|
+
"User-Agent" => "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.215 Safari/534.10"
|
17
|
+
}
|
18
|
+
|
19
|
+
class Quote
|
20
|
+
include ArgumentProcessor
|
21
|
+
|
22
|
+
attr_accessor :symbol, :last, :bid, :ask, :extra
|
23
|
+
|
24
|
+
def initialize(args = {})
|
25
|
+
process_args(args)
|
26
|
+
self.extra = {}
|
27
|
+
end
|
28
|
+
|
29
|
+
def mid
|
30
|
+
@mid ||= [bid, ask].reduce(:+) / 2.0
|
31
|
+
end
|
32
|
+
|
33
|
+
def inspect
|
34
|
+
"<%s: L:%s B:%s A:%s>" % [
|
35
|
+
symbol, last.to_money_s, bid.to_money_s, ask.to_money_s
|
36
|
+
]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class << self
|
41
|
+
def fetch(ticker, opts = {})
|
42
|
+
url = "http://finance.yahoo.com/q?s=%s" % ticker
|
43
|
+
puts "Fetching #{url}..." if $VERBOSE
|
44
|
+
|
45
|
+
doc = with_retry do
|
46
|
+
Nokogiri::HTML.parse(open(url, HEADERS).read)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Realtime last is at yfs_l90_sym, use if exists
|
50
|
+
if opts[:try_rt]
|
51
|
+
last = (doc.at_css("#yfs_l90_#{ticker.downcase}").text.to_f * 100) rescue nil
|
52
|
+
end
|
53
|
+
|
54
|
+
last ||= (doc.at_css("#yfs_l10_#{ticker.downcase}").text.to_f * 100)
|
55
|
+
bid = (doc.at_css("#yfs_b00_#{ticker.downcase}").text.to_f * 100) rescue 0
|
56
|
+
ask = (doc.at_css("#yfs_a00_#{ticker.downcase}").text.to_f * 100) rescue 0
|
57
|
+
|
58
|
+
quote = Quote.new(
|
59
|
+
:symbol => ticker, :last => last, :bid => bid, :ask => ask)
|
60
|
+
|
61
|
+
if opts[:extra]
|
62
|
+
name = doc.at('h1').text.scan(/(.*) \([\w-]+\)$/).to_s # strip trailing (SYMBOL)
|
63
|
+
mktcap = doc.at_css("#yfs_j10_#{ticker.downcase}").text rescue nil
|
64
|
+
|
65
|
+
divyield = doc.at("#table2 .end td").text.scan(/\((.*)\)/).to_s.to_f rescue nil
|
66
|
+
|
67
|
+
begin
|
68
|
+
pe_row = doc.at("#table2 tr:nth-child(6)")
|
69
|
+
pe_label, pe_data = pe_row.search("th,td").map{|e|e.text}
|
70
|
+
|
71
|
+
unless pe_label =~ %r(P/E)
|
72
|
+
puts "P/E label mismatch"
|
73
|
+
pe_data = nil
|
74
|
+
end
|
75
|
+
rescue
|
76
|
+
# nothing
|
77
|
+
end
|
78
|
+
|
79
|
+
sector, industry = doc.search("#company_details a").map{|e|e.text} rescue nil
|
80
|
+
|
81
|
+
quote.extra = {
|
82
|
+
:name => name,
|
83
|
+
:mktcap => mktcap,
|
84
|
+
:divyield => divyield,
|
85
|
+
:pe => pe_data.to_f,
|
86
|
+
:sector => sector,
|
87
|
+
:industry => industry
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
puts quote.inspect if $VERBOSE
|
92
|
+
|
93
|
+
quote
|
94
|
+
end
|
95
|
+
|
96
|
+
require 'json'
|
97
|
+
def gchain(ticker, expiry)
|
98
|
+
url = "http://www.google.com/finance/option_chain?q=%s&expd=%s&expm=%s&expy=%s&output=json"
|
99
|
+
url = url % [ticker.gsub(/-/, "."), expiry.day, expiry.month, expiry.year]
|
100
|
+
|
101
|
+
puts "Fetching #{url}..." if $VERBOSE
|
102
|
+
doc = with_retry do
|
103
|
+
JSON.parse(
|
104
|
+
open(url).read.
|
105
|
+
gsub(/(\:"\w+):/, '\1'). # Remove extra colon that appears inside strings
|
106
|
+
gsub('\\', ""). # Remove slashes
|
107
|
+
gsub(/(['"])?(\w+)(['"])?:/m, '"\2":'), # Quote each key
|
108
|
+
:symbolize_names => true)
|
109
|
+
end
|
110
|
+
|
111
|
+
doc[:calls].map do |call|
|
112
|
+
quote = Quote.new(
|
113
|
+
:symbol => call[:s],
|
114
|
+
:last => call[:p].to_f * 100,
|
115
|
+
:bid => call[:b].to_f * 100,
|
116
|
+
:ask => call[:a].to_f * 100
|
117
|
+
)
|
118
|
+
|
119
|
+
unless quote.symbol =~ /^\w+\d+[CP]\d+$/
|
120
|
+
puts "Dropping bad option #{quote.symbol.inspect}"
|
121
|
+
next
|
122
|
+
end
|
123
|
+
|
124
|
+
unless quote.bid > 0 and quote.ask > 0
|
125
|
+
puts "Option #{quote.symbol.inspect} has no bid or ask"
|
126
|
+
next
|
127
|
+
end
|
128
|
+
|
129
|
+
[call[:strike].to_f * 100, quote]
|
130
|
+
end.compact
|
131
|
+
end
|
132
|
+
|
133
|
+
def chain(ticker, expiry)
|
134
|
+
url = "http://finance.yahoo.com/q/op?s=%s&m=%s" % [ticker, expiry.strftime("%Y-%m")]
|
135
|
+
puts "Fetching #{url}..." if $VERBOSE
|
136
|
+
|
137
|
+
doc = with_retry do
|
138
|
+
Nokogiri::HTML.parse(open(url, HEADERS).read)
|
139
|
+
end
|
140
|
+
|
141
|
+
itm_call_data = doc.
|
142
|
+
search("//table[@class='yfnc_datamodoutline1'][1]//td[@class='yfnc_h']").
|
143
|
+
map { |e| e.text }
|
144
|
+
|
145
|
+
rows = itm_call_data.in_groups_of(8)
|
146
|
+
|
147
|
+
rows.map do |row|
|
148
|
+
strike = row[0].to_f * 100
|
149
|
+
symbol = row[1]
|
150
|
+
last = row[2].to_f * 100
|
151
|
+
bid = row[4].to_f * 100
|
152
|
+
ask = row[5].to_f * 100
|
153
|
+
|
154
|
+
raise "Expected symbol, got #{symbol.inspect}" unless symbol =~ /^\w+\d+[CP]\d+$/
|
155
|
+
|
156
|
+
# Only pick symbols that have the correct expiry
|
157
|
+
# Occurs when multiple series appear for the same month (i.e. weeklys)
|
158
|
+
next unless symbol =~ /#{expiry.strftime("%y%m%d")}/
|
159
|
+
|
160
|
+
quote = Quote.new(
|
161
|
+
:symbol => symbol, :last => last, :bid => bid, :ask => ask)
|
162
|
+
|
163
|
+
[strike, quote]
|
164
|
+
end.compact
|
165
|
+
end
|
166
|
+
|
167
|
+
def event?(ticker, on_or_before_date)
|
168
|
+
url = "http://finance.yahoo.com/q/ce?s=%s" % ticker
|
169
|
+
puts "Fetching #{url}..." if $VERBOSE
|
170
|
+
|
171
|
+
doc = with_retry do
|
172
|
+
Nokogiri::HTML.parse(open(url, HEADERS).read)
|
173
|
+
end
|
174
|
+
|
175
|
+
return false if doc.text =~ /There is no Company Events data/
|
176
|
+
|
177
|
+
fragment = doc.
|
178
|
+
search("//table[@class='yfnc_datamodoutline1'][1]//td[@class='yfnc_tabledata1']")
|
179
|
+
|
180
|
+
return false if fragment.text =~ /No Upcoming Events/i
|
181
|
+
|
182
|
+
events = fragment.map{|e|e.text}.in_groups_of(3)
|
183
|
+
|
184
|
+
events.any? do |date, event, _|
|
185
|
+
event_date = Date.strptime date, "%d-%b-%y"
|
186
|
+
|
187
|
+
event_date <= on_or_before_date
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def historical_prices(ticker, from = Date.today - 365)
|
192
|
+
to = Date.today
|
193
|
+
|
194
|
+
url = "http://ichart.finance.yahoo.com/table.csv?s=%s&a=%s&b=%s&c=%s&d=%se=%s&f=%sg=d&ignore=.csv"
|
195
|
+
url = url % [ticker, from.month - 1, from.day, from.year, to.month - 1, to.year, to.day]
|
196
|
+
|
197
|
+
puts "Fetching #{url}..." if $VERBOSE
|
198
|
+
|
199
|
+
csv = with_retry do
|
200
|
+
open(url, HEADERS).read
|
201
|
+
end
|
202
|
+
|
203
|
+
# [newest, ..., oldest]
|
204
|
+
csv.scan(/\d+\.\d+$/).map { |p| p.to_f }
|
205
|
+
end
|
206
|
+
|
207
|
+
def constituents(ticker, offset = 0, traverse = true)
|
208
|
+
ticker = "^#{ticker}" unless ticker =~ /^\^/
|
209
|
+
|
210
|
+
url = "http://finance.yahoo.com/q/cp?s=%s&c=%s" % [CGI.escape(ticker), offset]
|
211
|
+
puts "Fetching #{url}..." if $VERBOSE
|
212
|
+
|
213
|
+
doc = with_retry do
|
214
|
+
Nokogiri::HTML.parse(open(url, HEADERS).read)
|
215
|
+
end
|
216
|
+
|
217
|
+
symbols = doc.at("#yfncsumtab").search("tr td:first-child.yfnc_tabledata1").map{ |td| td.text }
|
218
|
+
next_link = doc.at("#yfncsumtab").at("//a[text()='Next']")
|
219
|
+
|
220
|
+
if next_link && traverse
|
221
|
+
return symbols + constituents(ticker, offset + 1)
|
222
|
+
end
|
223
|
+
|
224
|
+
return symbols
|
225
|
+
end
|
226
|
+
|
227
|
+
INDEXES = {}
|
228
|
+
|
229
|
+
def member_of?(ticker, index_ticker)
|
230
|
+
index_members = INDEXES[index_ticker] ||= constituents(index_ticker)
|
231
|
+
|
232
|
+
index_members.include?(ticker)
|
233
|
+
end
|
234
|
+
|
235
|
+
def with_retry(&block)
|
236
|
+
retries = 20
|
237
|
+
|
238
|
+
begin
|
239
|
+
timeout(5) do
|
240
|
+
block.call
|
241
|
+
end
|
242
|
+
rescue Exception
|
243
|
+
retries -= 1
|
244
|
+
unless retries.zero?
|
245
|
+
puts "Got error, retrying"
|
246
|
+
puts $!
|
247
|
+
retry
|
248
|
+
end
|
249
|
+
raise
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
data/lib/option.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
class Option
|
2
|
+
include ArgumentProcessor
|
3
|
+
|
4
|
+
attr_accessor :symbol, :stock, :strike, :expires, :price, :current_date
|
5
|
+
|
6
|
+
def initialize(args = {})
|
7
|
+
raise ArgumentError, "An option cannot be instantiated" if instance_of? Option
|
8
|
+
|
9
|
+
@current_date = nil
|
10
|
+
|
11
|
+
process_args(args)
|
12
|
+
end
|
13
|
+
|
14
|
+
def days_to_expiry(opening_date = current_date)
|
15
|
+
[0, (expires - opening_date).to_i].max
|
16
|
+
end
|
17
|
+
|
18
|
+
def expired?
|
19
|
+
expires < current_date
|
20
|
+
end
|
21
|
+
|
22
|
+
undef current_date
|
23
|
+
def current_date
|
24
|
+
@current_date || Date.today
|
25
|
+
end
|
26
|
+
|
27
|
+
def call?
|
28
|
+
false
|
29
|
+
end
|
30
|
+
|
31
|
+
def put?
|
32
|
+
false
|
33
|
+
end
|
34
|
+
|
35
|
+
def in_the_money?
|
36
|
+
raise NotImplementedError
|
37
|
+
end
|
38
|
+
|
39
|
+
def out_the_money?
|
40
|
+
!in_the_money?
|
41
|
+
end
|
42
|
+
|
43
|
+
# A specific sub state of being out the money.
|
44
|
+
def at_the_money?
|
45
|
+
stock.price == strike
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_s
|
49
|
+
[stock.symbol, float_if_needed(strike),
|
50
|
+
expires.strftime("%b %y").upcase, self.class.name.upcase].join(" ")
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_ticker_s
|
54
|
+
return symbol if symbol
|
55
|
+
|
56
|
+
call_or_put = call? ? "C" : "P"
|
57
|
+
|
58
|
+
dollar = (strike.to_i / 100).to_s.rjust(5, "0")
|
59
|
+
decimal = (strike.to_i % 100).to_s.ljust(3, "0")
|
60
|
+
|
61
|
+
[stock.symbol, expires.strftime("%y%m%d"), call_or_put, dollar, decimal].join.upcase
|
62
|
+
end
|
63
|
+
|
64
|
+
def ==(other)
|
65
|
+
other.stock == stock &&
|
66
|
+
other.strike == strike &&
|
67
|
+
other.expires == expires
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def float_if_needed(num)
|
73
|
+
num / 100.0 == num / 100 ? num / 100 : num / 100.0
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class Call < Option
|
78
|
+
def call?
|
79
|
+
true
|
80
|
+
end
|
81
|
+
|
82
|
+
def in_the_money?
|
83
|
+
stock.price > strike
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
class Put < Option
|
88
|
+
def put?
|
89
|
+
true
|
90
|
+
end
|
91
|
+
|
92
|
+
def in_the_money?
|
93
|
+
stock.price < strike
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
data/lib/penfold.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'core_ext'
|
2
|
+
require 'argument_processor'
|
3
|
+
|
4
|
+
require 'stock'
|
5
|
+
require 'option'
|
6
|
+
require 'commission'
|
7
|
+
|
8
|
+
require 'market'
|
9
|
+
|
10
|
+
require 'covered_call_position'
|
11
|
+
require 'covered_call_exit'
|
12
|
+
require 'covered_call_early_exit'
|
13
|
+
require 'covered_call_expiry_itm_exit'
|
14
|
+
require 'covered_call_expiry_otm_exit'
|
15
|
+
|
16
|
+
require 'black_scholes'
|
17
|
+
|
18
|
+
module Penfold
|
19
|
+
VERSION = '1.0.0'
|
20
|
+
end
|