penfold 1.0.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Manifest.txt +13 -1
- data/Rakefile +5 -0
- data/bin/constituents +11 -0
- data/bin/optiondate +17 -0
- data/db/init.rb +39 -0
- data/db/slurp.rb +202 -0
- data/db/sqliterc +4 -0
- data/lib/black_scholes.rb +146 -0
- data/lib/market.rb +36 -9
- data/lib/option_calendar.rb +56 -0
- data/lib/penfold.rb +1 -1
- data/portfolio.example.yml +16 -0
- data/symbols/README.txt +7 -0
- data/symbols/all-optionable.txt +3351 -0
- data/symbols/etfs.txt +337 -0
- data/symbols/weeklys.txt +29 -0
- metadata +69 -10
- data/portfolio.yml +0 -277
data/Manifest.txt
CHANGED
@@ -3,10 +3,16 @@ History.txt
|
|
3
3
|
Manifest.txt
|
4
4
|
README.rdoc
|
5
5
|
Rakefile
|
6
|
+
bin/constituents
|
7
|
+
bin/optiondate
|
6
8
|
bin/penfold
|
7
9
|
bin/penfold-position
|
8
10
|
bin/penfold-try
|
11
|
+
db/init.rb
|
12
|
+
db/slurp.rb
|
13
|
+
db/sqliterc
|
9
14
|
lib/argument_processor.rb
|
15
|
+
lib/black_scholes.rb
|
10
16
|
lib/commission.rb
|
11
17
|
lib/core_ext.rb
|
12
18
|
lib/covered_call_early_exit.rb
|
@@ -16,7 +22,13 @@ lib/covered_call_expiry_otm_exit.rb
|
|
16
22
|
lib/covered_call_position.rb
|
17
23
|
lib/market.rb
|
18
24
|
lib/option.rb
|
25
|
+
lib/option_calendar.rb
|
19
26
|
lib/penfold.rb
|
20
27
|
lib/stock.rb
|
21
|
-
portfolio.yml
|
28
|
+
portfolio.example.yml
|
29
|
+
symbols/README.txt
|
30
|
+
symbols/all-optionable.txt
|
31
|
+
symbols/etfs.txt
|
32
|
+
symbols/weeklys.txt
|
33
|
+
test/test_option_calendar.rb
|
22
34
|
test/test_penfold.rb
|
data/Rakefile
CHANGED
@@ -6,6 +6,11 @@ require 'hoe'
|
|
6
6
|
Hoe.spec 'penfold' do |p|
|
7
7
|
p.developer('Andrew A. Smith', 'andy@tinnedfruit.org')
|
8
8
|
p.readme_file = "README.rdoc"
|
9
|
+
p.extra_deps.push *[
|
10
|
+
%w(nokogiri >=0),
|
11
|
+
%w(penfold >=0),
|
12
|
+
%w(net-http-persistent >=0)
|
13
|
+
]
|
9
14
|
end
|
10
15
|
|
11
16
|
Hoe.add_include_dirs '.'
|
data/bin/constituents
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
abort "usage: #{File.basename($0)} [index]" unless ARGV[0]
|
4
|
+
|
5
|
+
LIB_DIR = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
|
6
|
+
$LOAD_PATH << LIB_DIR
|
7
|
+
|
8
|
+
require 'lib/penfold'
|
9
|
+
|
10
|
+
puts "# Generated at #{Time.now}"
|
11
|
+
puts Market.constituents(ARGV[0]).join("\n")
|
data/bin/optiondate
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# optiondate [num of days]
|
4
|
+
|
5
|
+
LIB_DIR = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
|
6
|
+
$LOAD_PATH << LIB_DIR
|
7
|
+
|
8
|
+
require 'lib/option_calendar'
|
9
|
+
|
10
|
+
unless ARGV.size == 1 and ARGV.to_s =~ /^\d+$/
|
11
|
+
abort "usage: #{File.basename($0)} [num of days]"
|
12
|
+
end
|
13
|
+
|
14
|
+
expiry = OptionCalendar.nearest_expiration(Date.today, ARGV[0].to_i.days)
|
15
|
+
|
16
|
+
puts expiry.strftime("%y%m%d")
|
17
|
+
|
data/db/init.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "sequel"
|
3
|
+
|
4
|
+
DB = Sequel.sqlite("options.sqlite")
|
5
|
+
|
6
|
+
DB.create_table :covered_calls do
|
7
|
+
primary_key :id
|
8
|
+
|
9
|
+
Date :quote_date, :null => false
|
10
|
+
Integer :days, :null => false
|
11
|
+
|
12
|
+
String :stock, :null => false
|
13
|
+
Float :stock_price, :null => false # last
|
14
|
+
|
15
|
+
String :option, :null => false
|
16
|
+
Float :option_price, :null => false # bid
|
17
|
+
|
18
|
+
Float :implied_volatility, :null => false
|
19
|
+
Float :prob_max_profit, :null => false
|
20
|
+
Float :prob_profit, :null => false
|
21
|
+
|
22
|
+
Float :downside_protection, :null => false
|
23
|
+
|
24
|
+
Float :period_return, :null => false
|
25
|
+
Float :annual_return, :null => false
|
26
|
+
|
27
|
+
Boolean :event, :null => false
|
28
|
+
|
29
|
+
String :label
|
30
|
+
String :mktcap
|
31
|
+
String :name
|
32
|
+
|
33
|
+
index :annual_return
|
34
|
+
index :downside_protection
|
35
|
+
index :prob_max_profit
|
36
|
+
|
37
|
+
unique [:quote_date, :stock, :option]
|
38
|
+
end
|
39
|
+
|
data/db/slurp.rb
ADDED
@@ -0,0 +1,202 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'sequel'
|
3
|
+
require 'penfold'
|
4
|
+
|
5
|
+
filename = ARGV[0]
|
6
|
+
|
7
|
+
symbols = if filename
|
8
|
+
File.read(filename).
|
9
|
+
split(/\n/).
|
10
|
+
map{|s| s.strip}.
|
11
|
+
reject{|s|s =~ /^#/}
|
12
|
+
else
|
13
|
+
%w(VZ C GE DVY PGR COCO BP RIG)
|
14
|
+
end
|
15
|
+
|
16
|
+
# series is the option symbol timestamp,
|
17
|
+
# such as 100821, 100918, etc.
|
18
|
+
series = ARGV[1]
|
19
|
+
expiry = Date.strptime series, "%y%m%d"
|
20
|
+
|
21
|
+
today = Date.today
|
22
|
+
|
23
|
+
quote_date = if [6,0].include?(today.wday)
|
24
|
+
# weekend, wind back to friday
|
25
|
+
today - (today.wday.zero? ? 2 : 1)
|
26
|
+
else
|
27
|
+
today
|
28
|
+
end
|
29
|
+
|
30
|
+
puts "Quote date is #{quote_date.inspect}"
|
31
|
+
|
32
|
+
INVESTMENT_AMOUNT = 10_000_00 # $10,000.00
|
33
|
+
|
34
|
+
# Do not reorder these, only append.
|
35
|
+
# DJI = 1
|
36
|
+
# OEX = 2
|
37
|
+
# SPX = 4
|
38
|
+
# ETFS = 8
|
39
|
+
# NONE = 16
|
40
|
+
INDEX_SYMBOLS = %w(DJI OEX SPX ETFS NONE)
|
41
|
+
|
42
|
+
DB = Sequel.sqlite("options.sqlite")
|
43
|
+
calls = DB[:covered_calls]
|
44
|
+
|
45
|
+
symbols.each do |symbol|
|
46
|
+
puts "== #{symbol} ====================="
|
47
|
+
|
48
|
+
begin
|
49
|
+
# Get a delayed quote, as option quotes are delayed.
|
50
|
+
stock_quote = Market.fetch(symbol, :try_rt => false, :extra => true)
|
51
|
+
rescue
|
52
|
+
puts "Error getting #{symbol}"
|
53
|
+
puts $!
|
54
|
+
next
|
55
|
+
end
|
56
|
+
|
57
|
+
stock = Stock.new(:symbol => symbol, :price => stock_quote.last)
|
58
|
+
|
59
|
+
num_shares = ((INVESTMENT_AMOUNT / stock.price) / 100.0).floor * 100
|
60
|
+
|
61
|
+
if num_shares.zero?
|
62
|
+
puts "Skipping because #{INVESTMENT_AMOUNT.to_money_s} will not buy any shares of #{symbol} @ #{stock.price.to_money_s}."
|
63
|
+
next
|
64
|
+
end
|
65
|
+
|
66
|
+
begin
|
67
|
+
itm_option_quotes = Market.chain(symbol, expiry)
|
68
|
+
rescue
|
69
|
+
puts "No options for #{symbol}"
|
70
|
+
puts $!
|
71
|
+
next
|
72
|
+
end
|
73
|
+
|
74
|
+
event = Market.event?(symbol, expiry)
|
75
|
+
|
76
|
+
begin
|
77
|
+
prices = Market.historical_prices(stock.symbol)
|
78
|
+
rescue
|
79
|
+
puts "No historical pricing available for #{symbol}"
|
80
|
+
puts $!
|
81
|
+
next
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
|
86
|
+
itm_options = itm_option_quotes.map do |strike, option_quote|
|
87
|
+
Call.new(
|
88
|
+
:symbol => option_quote.symbol,
|
89
|
+
:stock => stock,
|
90
|
+
:strike => strike,
|
91
|
+
:expires => expiry,
|
92
|
+
:price => option_quote.bid, # TODO: parametize
|
93
|
+
:current_date => quote_date
|
94
|
+
)
|
95
|
+
end
|
96
|
+
|
97
|
+
positions = itm_options.map do |option|
|
98
|
+
CoveredCallPosition.new(
|
99
|
+
:commission => "OptionsHouse",
|
100
|
+
:num_shares => num_shares,
|
101
|
+
:date_established => Date.today,
|
102
|
+
:option => option
|
103
|
+
)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Get the IV for the nearest ot the money ITM option. If this comes back as
|
107
|
+
# N/A, then the option is probably too far from the money to have an IV.
|
108
|
+
# (example C @ 3.95, ITM option is at 3 strike with few days left)
|
109
|
+
nearest_atm_iv = positions.last.implied_volatility rescue 0.0
|
110
|
+
|
111
|
+
positions.each { |p| p.instance_variable_set "@iv", nearest_atm_iv }
|
112
|
+
|
113
|
+
positions.each do |position|
|
114
|
+
close = CoveredCallExit.new(
|
115
|
+
:opening_position => position,
|
116
|
+
:exit_date => Date.today + 99999,
|
117
|
+
:stock_price => stock.price,
|
118
|
+
:option_price => position.option.price
|
119
|
+
)
|
120
|
+
|
121
|
+
# calculate percentage above strike for year
|
122
|
+
strike = position.option.strike / 100.0
|
123
|
+
above = prices.select { |price| price >= strike }
|
124
|
+
percent_above = above.size / prices.size.to_f
|
125
|
+
|
126
|
+
summary = <<-SUMMARY
|
127
|
+
%s @ %s: %s @ %s IV %s
|
128
|
+
Prob. (Max) profit (%s) %s
|
129
|
+
Downside Protection %s
|
130
|
+
Return (period) ann (%s) %s %s
|
131
|
+
SUMMARY
|
132
|
+
|
133
|
+
r = close.annualized_return
|
134
|
+
|
135
|
+
if r > 0.12
|
136
|
+
puts summary % [
|
137
|
+
stock.symbol,
|
138
|
+
stock.price.to_money_s,
|
139
|
+
position.option.to_ticker_s,
|
140
|
+
position.option.price.to_money_s,
|
141
|
+
|
142
|
+
position.implied_volatility.to_percent_s(2),
|
143
|
+
position.probability_max_profit.to_percent_s(2),
|
144
|
+
position.probability_profit.to_percent_s(2),
|
145
|
+
|
146
|
+
position.downside_protection.to_percent_s(2),
|
147
|
+
|
148
|
+
close.period_return.to_percent_s(2),
|
149
|
+
r.to_percent_s(2),
|
150
|
+
event ? "EVENT IN DURATION" : ""
|
151
|
+
]
|
152
|
+
|
153
|
+
|
154
|
+
flag = 1 << INDEX_SYMBOLS.index("NONE")
|
155
|
+
|
156
|
+
INDEX_SYMBOLS.each_with_index do |index_symbol, i|
|
157
|
+
next if index_symbol == "NONE"
|
158
|
+
|
159
|
+
member_of_index = if File.exists?(fn = "symbols/#{index_symbol.downcase}.txt")
|
160
|
+
File.read(fn).include?("\n#{stock.symbol}\n")
|
161
|
+
else
|
162
|
+
Market.member_of?(stock.symbol, index_symbol)
|
163
|
+
end
|
164
|
+
|
165
|
+
flag |= (1 << i) if member_of_index
|
166
|
+
end
|
167
|
+
|
168
|
+
mktcap = stock_quote.extra[:mktcap]
|
169
|
+
|
170
|
+
if mktcap
|
171
|
+
multiplier = mktcap.scan(/.$/).to_s
|
172
|
+
mktcap = mktcap.to_f
|
173
|
+
mktcap = multiplier == "B" ? mktcap * 1_000_000_000 : multiplier == "M" ? mktcap * 1_000_000 : nil
|
174
|
+
end
|
175
|
+
|
176
|
+
calls.insert(
|
177
|
+
:quote_date => quote_date,
|
178
|
+
:days => close.days_in_position,
|
179
|
+
:stock => stock.symbol,
|
180
|
+
:stock_price => stock.price / 100.0,
|
181
|
+
:option => position.option.to_ticker_s,
|
182
|
+
:option_price => position.option.price / 100.0,
|
183
|
+
:implied_volatility => position.implied_volatility * 100.0,
|
184
|
+
:prob_max_profit => position.probability_max_profit * 100.0,
|
185
|
+
:prob_profit => position.probability_profit * 100.0,
|
186
|
+
:downside_protection => position.downside_protection * 100.0,
|
187
|
+
:period_return => close.period_return * 100.0,
|
188
|
+
:annual_return => close.annualized_return * 100.0,
|
189
|
+
:event => event,
|
190
|
+
:label => File.split(filename).last,
|
191
|
+
:mktcap => mktcap,
|
192
|
+
:name => stock_quote.extra[:name],
|
193
|
+
:divyield => stock_quote.extra[:divyield],
|
194
|
+
:pe => stock_quote.extra[:pe],
|
195
|
+
:sector => stock_quote.extra[:sector],
|
196
|
+
:industry => stock_quote.extra[:industry],
|
197
|
+
:percent_above => percent_above * 100.0,
|
198
|
+
:memberships => flag
|
199
|
+
)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
data/db/sqliterc
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
# JavaScript adopted from Bernt Arne Odegaard's Financial Numerical Recipes
|
2
|
+
# http://finance.bi.no/~bernt/gcc_prog/algoritms/algoritms/algoritms.html
|
3
|
+
# by Steve Derezinski, CXWeb, Inc. http://www.cxweb.com
|
4
|
+
# Copyright (C) 1998 Steve Derezinski, Bernt Arne Odegaard
|
5
|
+
#
|
6
|
+
# This program is free software; you can redistribute it and/or
|
7
|
+
# modify it under the terms of the GNU General Public License
|
8
|
+
# as published by the Free Software Foundation.
|
9
|
+
#
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13
|
+
# GNU General Public License for more details.
|
14
|
+
# http://www.fsf.org/copyleft/gpl.html
|
15
|
+
|
16
|
+
class BlackScholes
|
17
|
+
class << self
|
18
|
+
def ndist(z)
|
19
|
+
(1.0/(Math.sqrt(2*Math::PI)))*Math.exp(-0.5*z)
|
20
|
+
end
|
21
|
+
|
22
|
+
def n(z)
|
23
|
+
b1 = 0.31938153
|
24
|
+
b2 = -0.356563782
|
25
|
+
b3 = 1.781477937
|
26
|
+
b4 = -1.821255978
|
27
|
+
b5 = 1.330274429
|
28
|
+
p = 0.2316419
|
29
|
+
c2 = 0.3989423
|
30
|
+
|
31
|
+
a = z.abs
|
32
|
+
|
33
|
+
return 1.0 if a > 6.0
|
34
|
+
|
35
|
+
t = 1.0/(1.0+a*p)
|
36
|
+
b = c2*Math.exp((-z)*(z/2.0))
|
37
|
+
n = ((((b5*t+b4)*t+b3)*t+b2)*t+b1)*t
|
38
|
+
n = 1.0-b*n
|
39
|
+
|
40
|
+
n = 1.0 - n if z < 0.0
|
41
|
+
|
42
|
+
n
|
43
|
+
end
|
44
|
+
|
45
|
+
def black_scholes(call,s,x,r,v,t)
|
46
|
+
# call = Boolean (to calc call, call=True, put: call=false)
|
47
|
+
# s = stock prics, x = strike price, r = no-risk interest rate
|
48
|
+
# v = volitility (1 std dev of s for (1 yr? 1 month?, you pick)
|
49
|
+
# t = time to maturity
|
50
|
+
|
51
|
+
sqt = Math.sqrt(t)
|
52
|
+
|
53
|
+
d1 = (Math.log(s/x) + r*t)/(v*sqt) + 0.5*(v*sqt)
|
54
|
+
d2 = d1 - (v*sqt)
|
55
|
+
|
56
|
+
if call
|
57
|
+
delta = n(d1)
|
58
|
+
nd2 = n(d2)
|
59
|
+
else # put
|
60
|
+
delta = -n(-d1)
|
61
|
+
nd2 = -n(-d2)
|
62
|
+
end
|
63
|
+
|
64
|
+
ert = Math.exp(-r*t)
|
65
|
+
nd1 = ndist(d1)
|
66
|
+
|
67
|
+
gamma = nd1/(s*v*sqt)
|
68
|
+
vega = s*sqt*nd1
|
69
|
+
theta = -(s*v*nd1)/(2*sqt) - r*x*ert*nd2
|
70
|
+
rho = x*t*ert*nd2
|
71
|
+
|
72
|
+
s*delta-x*ert*nd2
|
73
|
+
end
|
74
|
+
|
75
|
+
def option_implied_volatility(call,s,x,r,t,o)
|
76
|
+
# call = Boolean (to calc call, call=True, put: call=false)
|
77
|
+
# s = stock prics, x = strike price, r = no-risk interest rate
|
78
|
+
# t = time to maturity
|
79
|
+
# o = option price
|
80
|
+
|
81
|
+
sqt = Math.sqrt(t)
|
82
|
+
accuracy = 0.0001
|
83
|
+
|
84
|
+
sigma = (o/s)/(0.398*sqt)
|
85
|
+
|
86
|
+
100.times do
|
87
|
+
price = black_scholes(call,s,x,r,sigma,t)
|
88
|
+
diff = o-price
|
89
|
+
|
90
|
+
return sigma if diff.abs < accuracy
|
91
|
+
|
92
|
+
d1 = (Math.log(s/x) + r*t)/(sigma*sqt) + 0.5*sigma*sqt
|
93
|
+
vega = s*sqt*ndist(d1)
|
94
|
+
sigma = sigma+diff/vega
|
95
|
+
end
|
96
|
+
|
97
|
+
raise "Failed to converge"
|
98
|
+
end
|
99
|
+
|
100
|
+
def call_iv(s,x,r,t,o)
|
101
|
+
option_implied_volatility(true,s,x,r/100.0,t/365.0,o)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Returns probability of occuring below and above target price.
|
105
|
+
def probability(price, target, days, volatility)
|
106
|
+
p = price.to_f
|
107
|
+
q = target.to_f
|
108
|
+
t = days / 365.0
|
109
|
+
v = volatility.to_f
|
110
|
+
|
111
|
+
vt = v*Math.sqrt(t)
|
112
|
+
lnpq = Math.log(q/p)
|
113
|
+
|
114
|
+
d1 = lnpq / vt
|
115
|
+
|
116
|
+
y = (1/(1+0.2316419*d1.abs)*100000).floor / 100000.0
|
117
|
+
z = (0.3989423*Math.exp(-((d1*d1)/2))*100000).floor / 100000.0
|
118
|
+
|
119
|
+
y5 = 1.330274*(y**5)
|
120
|
+
y4 = 1.821256*(y**4)
|
121
|
+
y3 = 1.781478*(y**3)
|
122
|
+
y2 = 0.356538*(y**2)
|
123
|
+
y1 = 0.3193815*y
|
124
|
+
|
125
|
+
x = 1-z*(y5-y4+y3-y2+y1)
|
126
|
+
|
127
|
+
x = (x*100000).floor / 100000.0
|
128
|
+
|
129
|
+
x = 1-x if d1 < 0
|
130
|
+
|
131
|
+
pbelow = (x*1000).floor / 10.0
|
132
|
+
pabove = ((1-x)*1000).floor / 10.0;
|
133
|
+
|
134
|
+
[pbelow/100,pabove/100];
|
135
|
+
end
|
136
|
+
|
137
|
+
def probability_above(price, target, days, volatility)
|
138
|
+
probability(price, target, days, volatility)[1]
|
139
|
+
end
|
140
|
+
|
141
|
+
def probability_below(price, target, days, volatility)
|
142
|
+
probability(price, target, days, volatility)[0]
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
data/lib/market.rb
CHANGED
@@ -1,21 +1,27 @@
|
|
1
|
-
require 'rubygems'
|
2
|
-
require 'nokogiri'
|
3
|
-
require 'open-uri'
|
4
1
|
require 'date'
|
5
2
|
require 'cgi'
|
6
3
|
|
4
|
+
require 'stringio'
|
5
|
+
require 'zlib'
|
6
|
+
|
7
|
+
require 'rubygems'
|
8
|
+
require 'nokogiri'
|
9
|
+
require 'net/http/persistent'
|
10
|
+
|
7
11
|
class Market
|
8
12
|
HEADERS = {
|
9
13
|
"Accept" => "application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5",
|
10
14
|
"Accept-Charset" => "ISO-8859-1,utf-8;q=0.7,*;q=0.3",
|
11
15
|
"Accept-Language" => "en-US,en;q=0.8",
|
16
|
+
"Accept-Encoding" => "gzip,deflate",
|
12
17
|
"Cache-Control" => "max-age=0",
|
13
|
-
"Connection" => "keep-alive",
|
14
18
|
"Host" => "finance.yahoo.com",
|
15
19
|
"Referer" => "http://finance.yahoo.com/",
|
16
20
|
"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
21
|
}
|
18
22
|
|
23
|
+
@http = Net::HTTP::Persistent.new
|
24
|
+
|
19
25
|
class Quote
|
20
26
|
include ArgumentProcessor
|
21
27
|
|
@@ -38,12 +44,33 @@ class Market
|
|
38
44
|
end
|
39
45
|
|
40
46
|
class << self
|
47
|
+
def get(url)
|
48
|
+
uri = URI.parse(url)
|
49
|
+
|
50
|
+
request = Net::HTTP::Get.new uri.request_uri
|
51
|
+
|
52
|
+
HEADERS.each do |name, value|
|
53
|
+
request.add_field name, value
|
54
|
+
end
|
55
|
+
|
56
|
+
response = @http.request(uri, request)
|
57
|
+
data = response.body
|
58
|
+
|
59
|
+
out = case response["content-encoding"]
|
60
|
+
when /gzip/ then Zlib::GzipReader.new(StringIO.new(data)).read
|
61
|
+
when /deflate/ then Zlib::Inflate.inflate(data)
|
62
|
+
else data
|
63
|
+
end
|
64
|
+
|
65
|
+
out
|
66
|
+
end
|
67
|
+
|
41
68
|
def fetch(ticker, opts = {})
|
42
69
|
url = "http://finance.yahoo.com/q?s=%s" % ticker
|
43
70
|
puts "Fetching #{url}..." if $VERBOSE
|
44
71
|
|
45
72
|
doc = with_retry do
|
46
|
-
Nokogiri::HTML.parse(
|
73
|
+
Nokogiri::HTML.parse(get(url))
|
47
74
|
end
|
48
75
|
|
49
76
|
# Realtime last is at yfs_l90_sym, use if exists
|
@@ -135,7 +162,7 @@ class Market
|
|
135
162
|
puts "Fetching #{url}..." if $VERBOSE
|
136
163
|
|
137
164
|
doc = with_retry do
|
138
|
-
Nokogiri::HTML.parse(
|
165
|
+
Nokogiri::HTML.parse(get(url))
|
139
166
|
end
|
140
167
|
|
141
168
|
itm_call_data = doc.
|
@@ -169,7 +196,7 @@ class Market
|
|
169
196
|
puts "Fetching #{url}..." if $VERBOSE
|
170
197
|
|
171
198
|
doc = with_retry do
|
172
|
-
Nokogiri::HTML.parse(
|
199
|
+
Nokogiri::HTML.parse(get(url))
|
173
200
|
end
|
174
201
|
|
175
202
|
return false if doc.text =~ /There is no Company Events data/
|
@@ -197,7 +224,7 @@ class Market
|
|
197
224
|
puts "Fetching #{url}..." if $VERBOSE
|
198
225
|
|
199
226
|
csv = with_retry do
|
200
|
-
|
227
|
+
get(url)
|
201
228
|
end
|
202
229
|
|
203
230
|
# [newest, ..., oldest]
|
@@ -211,7 +238,7 @@ class Market
|
|
211
238
|
puts "Fetching #{url}..." if $VERBOSE
|
212
239
|
|
213
240
|
doc = with_retry do
|
214
|
-
Nokogiri::HTML.parse(
|
241
|
+
Nokogiri::HTML.parse(get(url))
|
215
242
|
end
|
216
243
|
|
217
244
|
symbols = doc.at("#yfncsumtab").search("tr td:first-child.yfnc_tabledata1").map{ |td| td.text }
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# Finds upcoming expiry dates for monthly equity options.
|
2
|
+
#
|
3
|
+
# Test data is the official CBOE expiration calendars for 2010/11.
|
4
|
+
|
5
|
+
require 'date'
|
6
|
+
|
7
|
+
require 'rubygems'
|
8
|
+
require 'active_support'
|
9
|
+
|
10
|
+
class OptionCalendar
|
11
|
+
class << self
|
12
|
+
FRIDAY = 5
|
13
|
+
|
14
|
+
def expiration_for(month)
|
15
|
+
start = month.beginning_of_month
|
16
|
+
|
17
|
+
expiry_week = start.advance(:days => 14)
|
18
|
+
|
19
|
+
expiry_week += 1.day while expiry_week.wday != FRIDAY
|
20
|
+
|
21
|
+
expiry_saturday = expiry_week + 1.day
|
22
|
+
end
|
23
|
+
|
24
|
+
def next_expiring_months(from = Date.today)
|
25
|
+
this_month = expiration_for(from)
|
26
|
+
next_month = expiration_for(from.next_month)
|
27
|
+
|
28
|
+
expirations = [
|
29
|
+
this_month,
|
30
|
+
next_month
|
31
|
+
]
|
32
|
+
|
33
|
+
# If the first month has already expired, remove it, and add another month out
|
34
|
+
if from >= (this_month - 1.day)
|
35
|
+
expirations.shift
|
36
|
+
expirations << expiration_for(next_month.next_month)
|
37
|
+
end
|
38
|
+
|
39
|
+
expirations
|
40
|
+
end
|
41
|
+
|
42
|
+
def nearest_expiration(date, range = 3.days)
|
43
|
+
expiration_date = expiration_for(date + range)
|
44
|
+
|
45
|
+
if date > expiration_date
|
46
|
+
expiration_date = expiration_for(expiration_date.next_month)
|
47
|
+
end
|
48
|
+
|
49
|
+
if date >= expiration_date - range
|
50
|
+
expiration_for(expiration_date.next_month)
|
51
|
+
else
|
52
|
+
expiration_date
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/penfold.rb
CHANGED
@@ -0,0 +1,16 @@
|
|
1
|
+
---
|
2
|
+
example: !ruby/object:CoveredCallPosition
|
3
|
+
commission: !ruby/object:Commission
|
4
|
+
option_assignment: 500
|
5
|
+
option_entry: 850
|
6
|
+
option_entry_per_contract: 15
|
7
|
+
stock_entry: 295
|
8
|
+
num_shares: 500
|
9
|
+
option: !ruby/object:Call
|
10
|
+
expires: 2010-08-24
|
11
|
+
price: 600
|
12
|
+
stock: !ruby/object:Stock
|
13
|
+
price: 5000
|
14
|
+
symbol: XYZ
|
15
|
+
strike: 5000
|
16
|
+
|