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 ADDED
@@ -0,0 +1,23 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'autotest/restart'
4
+
5
+ # Autotest.add_hook :initialize do |at|
6
+ # at.extra_files << "../some/external/dependency.rb"
7
+ #
8
+ # at.libs << ":../some/external"
9
+ #
10
+ # at.add_exception 'vendor'
11
+ #
12
+ # at.add_mapping(/dependency.rb/) do |f, _|
13
+ # at.files_matching(/test_.*rb$/)
14
+ # end
15
+ #
16
+ # %w(TestA TestB).each do |klass|
17
+ # at.extra_class_map[klass] = "test/test_misc.rb"
18
+ # end
19
+ # end
20
+
21
+ # Autotest.add_hook :run_command do |at|
22
+ # system "rake build"
23
+ # end
data/History.txt ADDED
@@ -0,0 +1,6 @@
1
+ === 1.0.0 / 2010-07-22
2
+
3
+ * 1 major enhancement
4
+
5
+ * Birthday!
6
+
data/Manifest.txt ADDED
@@ -0,0 +1,22 @@
1
+ .autotest
2
+ History.txt
3
+ Manifest.txt
4
+ README.rdoc
5
+ Rakefile
6
+ bin/penfold
7
+ bin/penfold-position
8
+ bin/penfold-try
9
+ lib/argument_processor.rb
10
+ lib/commission.rb
11
+ lib/core_ext.rb
12
+ lib/covered_call_early_exit.rb
13
+ lib/covered_call_exit.rb
14
+ lib/covered_call_expiry_itm_exit.rb
15
+ lib/covered_call_expiry_otm_exit.rb
16
+ lib/covered_call_position.rb
17
+ lib/market.rb
18
+ lib/option.rb
19
+ lib/penfold.rb
20
+ lib/stock.rb
21
+ portfolio.yml
22
+ test/test_penfold.rb
data/README.rdoc ADDED
@@ -0,0 +1,167 @@
1
+ = penfold
2
+
3
+ http://github.com/aasmith/penfold
4
+
5
+ == DESCRIPTION
6
+
7
+ Penfold is an assistant for screening potentital and tracking current
8
+ covered call positions.
9
+
10
+ == FEATURES
11
+
12
+ * Reports on the current profitability of a current position.
13
+ * Shows the potential profitability of hypothetical positions.
14
+
15
+ == TODO
16
+
17
+ * Add portfolio read/write frontend.
18
+ * Allow penfold-try to use a default ITM strike price.
19
+ * Some arguments to penfold-try are incomplete.
20
+ * Make commission class more flexible.
21
+
22
+ == USAGE
23
+
24
+ Usage: penfold [position|try|show|list|add|remove] [options]
25
+
26
+ === Position
27
+
28
+ Reports on a current position.
29
+
30
+ Usage: penfold position [[-n NAME|-A] exit options]
31
+
32
+ Position options:
33
+ -n, --name NAME Name of position in portfolio
34
+ -A, --all Use all positions in portfolio
35
+
36
+ Exit options:
37
+ -s, --exit-stock-price PRICE Stock price to exit at
38
+ -o, --exit-option-price PRICE Option price to exit at
39
+ -e, --exit-days DAYS Exit in n DAYS (default 0)
40
+ -E, --expires Hold position until expiry
41
+
42
+ Common options:
43
+ -c, --commission=NAME Commission fees to apply
44
+ -h, --help Show this message
45
+ -v, --verbose
46
+
47
+ === Try
48
+
49
+ Tries out a hypothetical position.
50
+
51
+ Usage: penfold try [[[ticker options] entry options] exit options]
52
+
53
+ Ticker options:
54
+ -t, --ticker=SYMBOL Stock ticker SYMBOL to try
55
+ -n, --num-shares=NUM Number of shares in position
56
+ -d, --option-date=DATE Expiry DATE of option contract
57
+ -x, --option-strike=PRICE Option strike PRICE
58
+
59
+ Entry options:
60
+ -s, --entry-stock-price=PRICE Stock PRICE to enter at
61
+ -o, --entry-option-price=PRICE Option PRICE to enter at
62
+
63
+ Exit options:
64
+ -S, --exit-stock-price=PRICE Stock PRICE to exit at
65
+ -O, --exit-option-price=PRICE Option PRICE to exit at
66
+ -e, --exit-days=DAYS Exit in n DAYS (default expiry)
67
+ -E, --expires Hold position until expiry
68
+
69
+ Common options:
70
+ -c, --commission=NAME Commission fees to apply
71
+ -L, --last Use last price instead of bid/ask for stock pricing
72
+ -h, --help Show this message
73
+ -v, --verbose
74
+
75
+
76
+ == EXAMPLES
77
+
78
+ # Commands for checking a current position
79
+
80
+ # check the profit of a position, using current market pricing
81
+ # assuming an immediate exit
82
+
83
+ penfold position --name=example
84
+
85
+ # check the profit of a position, using a hypothetical exit price,
86
+ # assuming an exit 3 days from now
87
+
88
+ penfold position
89
+ --name=example
90
+ --exit-stock-price=4.56
91
+ --exit-option-price=0.52
92
+ --exit-days=3
93
+
94
+ # check the profit of a position, using current market pricing
95
+ # assuming the expiry of the contract
96
+
97
+ penfold position --name=example --expires
98
+
99
+
100
+ # Commands for checking a potentital position
101
+
102
+ # check the profit of stock XYZ, with an August 2010 option with $4 strike,
103
+ # using current market pricing and assuming an expiry of the contract
104
+
105
+ penfold try --stock=XYZ --num-shares=1000 --option-date=100821 --option-strike=4
106
+
107
+ # check the profit of stock XYZ with an August 2010 option with $4 strike,
108
+ # using provided entry pricing and assuming an exit in 5 days with provided
109
+ # exit pricing
110
+
111
+ penfold try
112
+ --stock=XYZ --num-shares=1000 --option-date=100821 --option-strike=4
113
+ --stock-entry-price=4.25 --option-entry-price=0.35
114
+ --exit-days=5 --stock-exit-price=4.51 --option-exit-price=0.39
115
+
116
+
117
+ # Batch position checks
118
+
119
+ # Show all positions assuming expiry with current market prices
120
+ penfold position --all --expires
121
+
122
+ # Show all positions assuming immediate exit with current market prices
123
+ penfold position --all
124
+
125
+
126
+ # Portfolio commands
127
+
128
+ # List current portfolio positions
129
+
130
+ penfold [show|list]
131
+
132
+ penfold add example
133
+ --stock=XYZ --num-shares=1000 --option-date=100821 --option-strike=4
134
+ --stock-entry-price=4.5 --option-entry-price=0.34
135
+
136
+ penfold remove example
137
+
138
+ penfold rename example new_example
139
+
140
+
141
+ == REQUIREMENTS
142
+
143
+ * nokogiri, if fetching quotes
144
+
145
+ == LICENSE
146
+
147
+ Copyright (c) 2010 Andrew A. Smith
148
+
149
+ Permission is hereby granted, free of charge, to any person obtaining
150
+ a copy of this software and associated documentation files (the
151
+ 'Software'), to deal in the Software without restriction, including
152
+ without limitation the rights to use, copy, modify, merge, publish,
153
+ distribute, sublicense, and/or sell copies of the Software, and to
154
+ permit persons to whom the Software is furnished to do so, subject to
155
+ the following conditions:
156
+
157
+ The above copyright notice and this permission notice shall be
158
+ included in all copies or substantial portions of the Software.
159
+
160
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
161
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
162
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
163
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
164
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
165
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
166
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
167
+
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+
6
+ Hoe.spec 'penfold' do |p|
7
+ p.developer('Andrew A. Smith', 'andy@tinnedfruit.org')
8
+ p.readme_file = "README.rdoc"
9
+ end
10
+
11
+ Hoe.add_include_dirs '.'
12
+
13
+ # vim: syntax=Ruby
data/bin/penfold ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'abbrev'
4
+
5
+ names = %w(position try show list add remove)
6
+
7
+ name = names.abbrev[ARGV.first]
8
+
9
+ if name
10
+ exec "#{__FILE__}-#{name}", *ARGV
11
+ else
12
+ abort "Usage: penfold [#{names.join("|")}] [options]"
13
+ end
14
+
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'yaml'
5
+
6
+ LIB_DIR = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
7
+ $LOAD_PATH << LIB_DIR
8
+
9
+ require 'lib/penfold'
10
+
11
+ class Parser
12
+ Options = Struct.new(
13
+ :portfolio, :all, :name, :exit_stock_price, :exit_option_price, :days, :commission, :last
14
+ )
15
+
16
+ def self.parse(args)
17
+ options = Options.new
18
+ options.portfolio = "portfolio.yml"
19
+ options.days = 0
20
+
21
+ parser = OptionParser.new do |o|
22
+
23
+ o.banner = "Usage: penfold position [[-n NAME|-A] exit options]"
24
+
25
+ o.separator ""
26
+ o.separator "Position options:"
27
+
28
+ o.on("-n", "--name NAME", "Name of position in portfolio", /\w+/) do |name|
29
+ options.name = name
30
+ end
31
+
32
+ o.on("-A", "--all", "Use all positions in portfolio") do
33
+ options.all = true
34
+ end
35
+
36
+ o.separator ""
37
+ o.separator "Exit options:"
38
+
39
+ o.on("-s", "--exit-stock-price PRICE", "Stock price to exit at", Float) do |price|
40
+ options.exit_stock_price = price * 100
41
+ end
42
+
43
+ o.on("-o", "--exit-option-price PRICE", "Option price to exit at", Float) do |price|
44
+ options.exit_option_price = price * 100
45
+ end
46
+
47
+ o.on("-e", "--exit-days DAYS", "Exit in n DAYS (default 0)", Integer) do |days|
48
+ options.days = days
49
+ end
50
+
51
+ o.on("-E", "--expires", "Hold position until expiry") do
52
+ options.days = 999999
53
+ end
54
+
55
+ o.separator ""
56
+ o.separator "Common options:"
57
+
58
+ o.on("-c", "--commission=NAME", "Commission fees to apply", /\w+/) do |name|
59
+ options.commission = Commission.const_get(name)
60
+ end
61
+
62
+ o.on("-L", "--last", "Use last price instead of bid/ask for ",
63
+ "stock pricing. Best used when the market ",
64
+ "is closed.") do
65
+ options.last = true
66
+ end
67
+
68
+ o.on_tail("-h", "--help", "Show this message") do
69
+ puts o
70
+ exit
71
+ end
72
+
73
+ o.on_tail("-v", "--verbose") do
74
+ $VERBOSE = true
75
+ end
76
+ end
77
+
78
+ parser.parse!(args)
79
+ options
80
+ end
81
+ end
82
+
83
+ options = Parser.parse(ARGV) rescue abort($!.message)
84
+
85
+ portfolio = YAML.load(File.read(options.portfolio))
86
+
87
+ positions = if options.all or options.name.nil?
88
+ portfolio
89
+ else
90
+ [[options.name, YAML.load(File.read(options.portfolio))[options.name]]]
91
+ end
92
+
93
+ positions.each do |position_name, position|
94
+ position.commission = options.commission if options.commission
95
+
96
+ begin
97
+ exit_option_price =
98
+ options.exit_option_price ||
99
+ Market.fetch(position.option.to_ticker_s).ask
100
+
101
+ exit_stock_price =
102
+ options.exit_stock_price ||
103
+ Market.fetch(position.stock.symbol.upcase).
104
+ send(options.last ? :last : :bid)
105
+
106
+ rescue => e
107
+ puts e if $VERBOSE
108
+ puts "Unable to fetch data for position #{position_name}, skipping"
109
+ next
110
+ end
111
+
112
+ closing_position = CoveredCallExit.new(
113
+ :opening_position => position,
114
+ :exit_date => Date.today + options.days,
115
+ :stock_price => exit_stock_price,
116
+ :option_price => exit_option_price
117
+ )
118
+
119
+ output = <<EOT
120
+ Position: %s
121
+ Entry: %s @ %s, %s @ %s [spread %s]
122
+ IV %.2f%% Probability (Max) Profit (%.2f%%) %.2f%%
123
+
124
+ Stock Total %s (inc %s comm)
125
+ - Call Sale %s (inc %s comm)
126
+ = Net Outlay %s (%s per share)
127
+
128
+ Downside Protection: %s
129
+
130
+ Exit: %s @ %s, %s @ %s on %s [spread %s]
131
+
132
+ #{closing_position.explain.chomp}
133
+
134
+ Period Return: %s
135
+ Annualized Return: %s
136
+ Days in Position: %s
137
+
138
+ EOT
139
+
140
+ puts output % [
141
+ (position_name + " ").ljust(70, "="),
142
+ position.option,
143
+ position.option.price.to_money_s,
144
+ position.stock.symbol,
145
+ position.stock.price.to_money_s,
146
+ (position.stock.price - position.option.price).to_money_s,
147
+
148
+ position.implied_volatility * 100,
149
+ position.probability_max_profit * 100,
150
+ position.probability_profit * 100,
151
+
152
+ position.stock_total.to_money_s.rjust(12),
153
+ position.commission.stock_entry.to_money_s,
154
+ position.call_sale.to_money_s.rjust(12),
155
+ position.commission.option_entry.to_money_s,
156
+ position.net_outlay.to_money_s.rjust(12),
157
+ position.net_per_share.to_money_s,
158
+
159
+ position.downside_protection.to_percent_s,
160
+
161
+ closing_position.option,
162
+ closing_position.option.price.to_money_s,
163
+ closing_position.stock.symbol,
164
+ closing_position.stock.price.to_money_s,
165
+ closing_position.exit_date,
166
+ (closing_position.stock.price - closing_position.option.price).to_money_s,
167
+
168
+ closing_position.period_return.to_percent_s,
169
+ closing_position.annualized_return.to_percent_s,
170
+ closing_position.days_in_position
171
+ ]
172
+
173
+ end
data/bin/penfold-try ADDED
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'yaml'
5
+
6
+ LIB_DIR = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
7
+ $LOAD_PATH << LIB_DIR
8
+
9
+ require 'lib/penfold'
10
+
11
+ class Parser
12
+ Options = Struct.new(
13
+ :symbol, :num_shares, :option_date, :option_strike,
14
+ :entry_stock_price, :entry_option_price,
15
+ :exit_stock_price, :exit_option_price, :days,
16
+ :commission, :last
17
+ )
18
+
19
+ def self.parse(args)
20
+ options = Options.new
21
+ options.days = 999999
22
+ options.commission = Commission::FREE
23
+
24
+ parser = OptionParser.new do |o|
25
+ o.banner = "Usage: penfold try [[[ticker options] entry options] exit options]"
26
+
27
+ o.separator ""
28
+ o.separator "Ticker options:"
29
+
30
+ o.on("-t", "--ticker=SYMBOL", "Stock ticker SYMBOL to try", /\w+/) do |symbol|
31
+ options.symbol = symbol
32
+ end
33
+
34
+ o.on("-n", "--num-shares=NUM", "Number of shares in position", Integer) do |num_shares|
35
+ options.num_shares = num_shares
36
+ end
37
+
38
+ o.on("-d", "--option-date=DATE", "Expiry DATE of option contract", Integer) do |date|
39
+ date = date.to_s
40
+ date = "20#{date}" unless date =~ /^20/
41
+
42
+ options.option_date = Date.parse(date)
43
+ end
44
+
45
+ o.on("-x", "--option-strike=PRICE", "Option strike PRICE", Float) do |price|
46
+ options.option_strike = price * 100
47
+ end
48
+
49
+ o.separator ""
50
+ o.separator "Entry options:"
51
+
52
+ o.on("-s", "--entry-stock-price=PRICE", "Stock PRICE to enter at", Float) do |price|
53
+ options.entry_stock_price = price * 100
54
+ end
55
+
56
+ o.on("-o", "--entry-option-price=PRICE", "Option PRICE to enter at", Float) do |price|
57
+ options.entry_option_price = price * 100
58
+ end
59
+
60
+ o.separator ""
61
+ o.separator "Exit options:"
62
+
63
+ o.on("-S", "--exit-stock-price=PRICE", "Stock PRICE to exit at", Float) do |price|
64
+ options.exit_stock_price = price * 100
65
+ end
66
+
67
+ o.on("-O", "--exit-option-price=PRICE", "Option PRICE to exit at", Float) do |price|
68
+ options.exit_option_price = price * 100
69
+ end
70
+
71
+ o.on("-e", "--exit-days=DAYS", "Exit in n DAYS (default expiry)", Integer) do |days|
72
+ options.days = days
73
+ end
74
+
75
+ o.on("-E", "--expires", "Hold position until expiry") do
76
+ options.days = 999999
77
+ end
78
+
79
+ o.separator ""
80
+ o.separator "Common options:"
81
+
82
+ o.on("-c", "--commission=NAME", "Commission fees to apply", /\w+/) do |name|
83
+ options.commission = Commission.const_get(name)
84
+ end
85
+
86
+ o.on("-L", "--last", "Use last price instead of bid/ask for stock pricing") do
87
+ options.last = true
88
+ end
89
+
90
+ o.on_tail("-h", "--help", "Show this message") do
91
+ puts o
92
+ exit
93
+ end
94
+
95
+ o.on_tail("-v", "--verbose") do
96
+ $VERBOSE = true
97
+ end
98
+ end
99
+
100
+ parser.parse!(args)
101
+ options
102
+ end
103
+ end
104
+
105
+ options = Parser.parse(ARGV) rescue abort($!.message)
106
+
107
+ unless options.symbol && options.num_shares && options.option_date && options.option_strike
108
+ abort "Must provide all of -t, -n, -d, -x"
109
+ end
110
+
111
+ options.entry_stock_price ||= Market.fetch(options.symbol).send(options.last ? :last : :ask)
112
+
113
+ stock = Stock.new(
114
+ :symbol => options.symbol,
115
+ :price => options.entry_stock_price
116
+ )
117
+
118
+ call = Call.new(
119
+ :stock => stock,
120
+ :strike => options.option_strike,
121
+ :expires => options.option_date
122
+ )
123
+
124
+ call.price = options.entry_option_price || Market.fetch(call.to_ticker_s).bid
125
+
126
+ opening_position = CoveredCallPosition.new(
127
+ :num_shares => options.num_shares,
128
+ :date_established => Date.today,
129
+ :option => call,
130
+ :commission => options.commission
131
+ )
132
+
133
+ closing_position = CoveredCallExit.new(
134
+ :opening_position => opening_position,
135
+ :exit_date => Date.today + options.days,
136
+ :stock_price => options.exit_stock_price || options.entry_stock_price,
137
+ :option_price => options.exit_option_price || options.entry_option_price
138
+ )
139
+
140
+ output = <<EOT
141
+ Entry: %s @ %s, %s @ %s
142
+
143
+ Stock Total %s (inc %s comm)
144
+ - Call Sale %s (inc %s comm)
145
+ = Net Outlay %s (%s per share)
146
+
147
+ Downside Protection: %s
148
+
149
+ Exit: %s @ %s, %s @ %s on %s
150
+
151
+ #{closing_position.explain.chomp}
152
+
153
+ Period Return: %s
154
+ Annualized Return: %s
155
+ Days in Position: %s
156
+
157
+ EOT
158
+
159
+ puts output % [
160
+ opening_position.option,
161
+ opening_position.option.price.to_money_s,
162
+ opening_position.stock.symbol,
163
+ opening_position.stock.price.to_money_s,
164
+ opening_position.stock_total.to_money_s.rjust(12),
165
+ opening_position.commission.stock_entry.to_money_s,
166
+ opening_position.call_sale.to_money_s.rjust(12),
167
+ opening_position.commission.total_option_entry(opening_position.num_shares).to_money_s,
168
+ opening_position.net_outlay.to_money_s.rjust(12),
169
+ opening_position.net_per_share.to_money_s,
170
+
171
+ opening_position.downside_protection.to_percent_s,
172
+
173
+ closing_position.option,
174
+ closing_position.option.price.to_money_s,
175
+ closing_position.stock.symbol,
176
+ closing_position.stock.price.to_money_s,
177
+ closing_position.exit_date,
178
+
179
+ closing_position.period_return.to_percent_s,
180
+ closing_position.annualized_return.to_percent_s,
181
+ closing_position.days_in_position
182
+ ]
183
+
184
+
@@ -0,0 +1,6 @@
1
+ module ArgumentProcessor
2
+ def process_args(hash)
3
+ hash.each { |k,v| send(:"#{k}=", v) }
4
+ end
5
+ end
6
+
data/lib/commission.rb ADDED
@@ -0,0 +1,44 @@
1
+ class Commission
2
+ include ArgumentProcessor
3
+
4
+ attr_accessor :shares, :contracts
5
+
6
+ def initialize(args = {})
7
+ if instance_of? Commission
8
+ raise ArgumentError, "Commission cannot be instantiated"
9
+ end
10
+
11
+ process_args(args)
12
+
13
+ @shares ||= 0
14
+ @contracts ||= 0
15
+ end
16
+ end
17
+
18
+ class Commission::Free < Commission
19
+ def option_entry; 0 end
20
+ def stock_entry; 0 end
21
+ def option_assignment; 0 end
22
+ end
23
+
24
+ class Commission::OptionsHouse < Commission
25
+ def option_entry
26
+ contracts.zero? ? 0 : 8_50 + (contracts * 15)
27
+ end
28
+
29
+ def stock_entry
30
+ shares.zero? ? 0 : 2_95
31
+ end
32
+
33
+ def option_assignment
34
+ contracts.zero? ? 0 : 5_00
35
+ end
36
+ end
37
+
38
+ class Commission::OptionsHouseAlt < Commission::OptionsHouse
39
+ def option_entry
40
+ return 0 if contracts.zero?
41
+
42
+ contracts <= 5 ? 5_00 : contracts * 1_00
43
+ end
44
+ end
data/lib/core_ext.rb ADDED
@@ -0,0 +1,32 @@
1
+ require 'date'
2
+
3
+ class Date
4
+ undef inspect
5
+ def inspect
6
+ "#<Date: #{strftime("%c")}>"
7
+ end
8
+ end
9
+
10
+ class String
11
+ def commify
12
+ reverse.gsub(/(\d\d\d)(?=\d)(?!\d*\.)/, '\1,').reverse
13
+ end
14
+ end
15
+
16
+ class Numeric
17
+ def to_money_s
18
+ ("$%g" % (self / 100.0)).commify.sub(/\.(\d)\Z/, '.\10')
19
+ end
20
+
21
+ def to_percent_s(p = nil)
22
+ (p ? "%.#{p}f%%" : "%g%%") % (self * 100)
23
+ end
24
+ end
25
+
26
+ class Array
27
+ def in_groups_of(n)
28
+ raise ArgumentError, "Data is not in multiples of #{n}" unless size % n == 0
29
+
30
+ inject([[]]) { |a,e| (a.last.size == n) ? (a << [e]) : (a.last << e); a }
31
+ end
32
+ end