penfold 1.0.0 → 1.0.1
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/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
|
+
|