penfold 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/portfolio.yml
ADDED
@@ -0,0 +1,277 @@
|
|
1
|
+
---
|
2
|
+
#C: !ruby/object:CoveredCallPosition
|
3
|
+
# commission: OptionsHouseAlt
|
4
|
+
# num_shares: 2500
|
5
|
+
# date_established: 2010-07-19
|
6
|
+
# option: !ruby/object:Call
|
7
|
+
# expires: 2011-01-22
|
8
|
+
# price: 50
|
9
|
+
# stock: !ruby/object:Stock
|
10
|
+
# price: 395
|
11
|
+
# symbol: C
|
12
|
+
# strike: 400
|
13
|
+
|
14
|
+
#S: !ruby/object:CoveredCallPosition
|
15
|
+
# commission: OptionsHouseAlt
|
16
|
+
# num_shares: 2000
|
17
|
+
# date_established: 2010-07-19
|
18
|
+
# option: !ruby/object:Call
|
19
|
+
# expires: 2011-01-22
|
20
|
+
# price: 118
|
21
|
+
# stock: !ruby/object:Stock
|
22
|
+
# price: 471
|
23
|
+
# symbol: S
|
24
|
+
# strike: 400
|
25
|
+
|
26
|
+
#LYG: !ruby/object:CoveredCallPosition
|
27
|
+
# commission: OptionsHouseAlt
|
28
|
+
# num_shares: 400
|
29
|
+
# date_established: 2010-07-22
|
30
|
+
# option: !ruby/object:Call
|
31
|
+
# expires: 2010-08-21
|
32
|
+
# price: 11
|
33
|
+
# stock: !ruby/object:Stock
|
34
|
+
# price: 371
|
35
|
+
# symbol: LYG
|
36
|
+
# strike: 400
|
37
|
+
|
38
|
+
#CBS: !ruby/object:CoveredCallPosition
|
39
|
+
# commission: OptionsHouseAlt
|
40
|
+
# num_shares: 100
|
41
|
+
# date_established: 2010-07-29
|
42
|
+
# option: !ruby/object:Call
|
43
|
+
# expires: 2010-08-21
|
44
|
+
# price: 96
|
45
|
+
# stock: !ruby/object:Stock
|
46
|
+
# price: 1449
|
47
|
+
# symbol: CBS
|
48
|
+
# strike: 1400
|
49
|
+
|
50
|
+
#RIG: !ruby/object:CoveredCallPosition
|
51
|
+
# commission: OptionsHouseAlt
|
52
|
+
# num_shares: 200
|
53
|
+
# date_established: 2010-08-05
|
54
|
+
# option: !ruby/object:Call
|
55
|
+
# expires: 2010-08-06
|
56
|
+
# price: 225
|
57
|
+
# stock: !ruby/object:Stock
|
58
|
+
# price: 5681
|
59
|
+
# symbol: RIG
|
60
|
+
# strike: 5500
|
61
|
+
|
62
|
+
#VXX: !ruby/object:CoveredCallPosition
|
63
|
+
# commission: OptionsHouseAlt
|
64
|
+
# num_shares: 200
|
65
|
+
# date_established: 2010-08-05
|
66
|
+
# option: !ruby/object:Call
|
67
|
+
# expires: 2010-08-21
|
68
|
+
# price: 187
|
69
|
+
# stock: !ruby/object:Stock
|
70
|
+
# price: 2143
|
71
|
+
# symbol: VXX
|
72
|
+
# strike: 2000
|
73
|
+
|
74
|
+
#WY: !ruby/object:CoveredCallPosition
|
75
|
+
# commission: OptionsHouseAlt
|
76
|
+
# num_shares: 600
|
77
|
+
# date_established: 2010-08-13
|
78
|
+
# option: !ruby/object:Call
|
79
|
+
# expires: 2010-08-21
|
80
|
+
# price: 80
|
81
|
+
# stock: !ruby/object:Stock
|
82
|
+
# price: 1651
|
83
|
+
# symbol: WY
|
84
|
+
# strike: 1600
|
85
|
+
|
86
|
+
#ING: !ruby/object:CoveredCallPosition
|
87
|
+
# commission: OptionsHouseAlt
|
88
|
+
# num_shares: 1000
|
89
|
+
# date_established: 2010-08-11
|
90
|
+
# option: !ruby/object:Call
|
91
|
+
# expires: 2010-09-18
|
92
|
+
# price: 50
|
93
|
+
# stock: !ruby/object:Stock
|
94
|
+
# price: 886
|
95
|
+
# symbol: ING
|
96
|
+
# strike: 900
|
97
|
+
|
98
|
+
BCS: !ruby/object:CoveredCallPosition
|
99
|
+
commission: OptionsHouseAlt
|
100
|
+
num_shares: 500
|
101
|
+
date_established: 2010-08-02
|
102
|
+
option: !ruby/object:Call
|
103
|
+
expires: 2010-12-18
|
104
|
+
price: 30
|
105
|
+
stock: !ruby/object:Stock
|
106
|
+
price: 1966
|
107
|
+
symbol: BCS
|
108
|
+
strike: 2100
|
109
|
+
|
110
|
+
# DOW: !ruby/object:CoveredCallPosition
|
111
|
+
# commission: OptionsHouseAlt
|
112
|
+
# num_shares: 400
|
113
|
+
# date_established: 2010-08-02
|
114
|
+
# option: !ruby/object:Call
|
115
|
+
# expires: 2010-10-16
|
116
|
+
# price: 22
|
117
|
+
# stock: !ruby/object:Stock
|
118
|
+
# price: 2714
|
119
|
+
# symbol: DOW
|
120
|
+
# strike: 2800
|
121
|
+
|
122
|
+
#F: !ruby/object:CoveredCallPosition
|
123
|
+
# commission: OptionsHouseAlt
|
124
|
+
# num_shares: 900
|
125
|
+
# date_established: 2010-08-04
|
126
|
+
# option: !ruby/object:Call
|
127
|
+
# expires: 2010-09-18
|
128
|
+
# price: 33
|
129
|
+
# stock: !ruby/object:Stock
|
130
|
+
# price: 1189
|
131
|
+
# symbol: F
|
132
|
+
# strike: 1200
|
133
|
+
|
134
|
+
#GRMN: !ruby/object:CoveredCallPosition
|
135
|
+
# commission: OptionsHouseAlt
|
136
|
+
# num_shares: 400
|
137
|
+
# date_established: 2010-08-04
|
138
|
+
# option: !ruby/object:Call
|
139
|
+
# expires: 2010-09-18
|
140
|
+
# price: 64
|
141
|
+
# stock: !ruby/object:Stock
|
142
|
+
# price: 2760
|
143
|
+
# symbol: GRMN
|
144
|
+
# strike: 2800
|
145
|
+
|
146
|
+
#BP: !ruby/object:CoveredCallPosition
|
147
|
+
# commission: OptionsHouseAlt
|
148
|
+
# num_shares: 300
|
149
|
+
# date_established: 2010-08-30
|
150
|
+
# option: !ruby/object:Call
|
151
|
+
# expires: 2010-09-03
|
152
|
+
# price: 122
|
153
|
+
# stock: !ruby/object:Stock
|
154
|
+
# price: 3589
|
155
|
+
# symbol: BP
|
156
|
+
# strike: 3500
|
157
|
+
|
158
|
+
#BP2: !ruby/object:CoveredCallPosition
|
159
|
+
# commission: OptionsHouseAlt
|
160
|
+
# num_shares: 300
|
161
|
+
# date_established: 2010-09-02
|
162
|
+
# option: !ruby/object:Call
|
163
|
+
# expires: 2010-09-10
|
164
|
+
# price: 48
|
165
|
+
# stock: !ruby/object:Stock
|
166
|
+
# price: 3640
|
167
|
+
# symbol: BP
|
168
|
+
# strike: 3700
|
169
|
+
|
170
|
+
#RIG: !ruby/object:CoveredCallPosition
|
171
|
+
# commission: OptionsHouseAlt
|
172
|
+
# num_shares: 200
|
173
|
+
# date_established: 2010-09-08
|
174
|
+
# option: !ruby/object:Call
|
175
|
+
# expires: 2010-09-18
|
176
|
+
# price: 242
|
177
|
+
# stock: !ruby/object:Stock
|
178
|
+
# price: 5404
|
179
|
+
# symbol: RIG
|
180
|
+
# strike: 5250
|
181
|
+
|
182
|
+
#WFC: !ruby/object:CoveredCallPosition
|
183
|
+
# commission: OptionsHouseAlt
|
184
|
+
# num_shares: 400
|
185
|
+
# date_established: 2010-09-13
|
186
|
+
# option: !ruby/object:Call
|
187
|
+
# expires: 2010-09-18
|
188
|
+
# price: 55
|
189
|
+
# stock: !ruby/object:Stock
|
190
|
+
# price: 2629
|
191
|
+
# symbol: WFC
|
192
|
+
# strike: 2600
|
193
|
+
|
194
|
+
# ING: !ruby/object:CoveredCallPosition
|
195
|
+
# commission: OptionsHouseAlt
|
196
|
+
# num_shares: 1000
|
197
|
+
# date_established: 2010-09-21
|
198
|
+
# option: !ruby/object:Call
|
199
|
+
# expires: 2010-10-16
|
200
|
+
# price: 55
|
201
|
+
# stock: !ruby/object:Stock
|
202
|
+
# price: 1026
|
203
|
+
# symbol: ING
|
204
|
+
# strike: 1000
|
205
|
+
|
206
|
+
#BP: !ruby/object:CoveredCallPosition
|
207
|
+
# commission: OptionsHouseAlt
|
208
|
+
# num_shares: 300
|
209
|
+
# date_established: 2010-09-27
|
210
|
+
# option: !ruby/object:Call
|
211
|
+
# expires: 2010-10-01
|
212
|
+
# price: 74
|
213
|
+
# stock: !ruby/object:Stock
|
214
|
+
# price: 3856
|
215
|
+
# symbol: BP
|
216
|
+
# strike: 3800
|
217
|
+
|
218
|
+
JPM: !ruby/object:CoveredCallPosition
|
219
|
+
commission: OptionsHouseAlt
|
220
|
+
num_shares: 300
|
221
|
+
date_established: 2010-09-27
|
222
|
+
option: !ruby/object:Call
|
223
|
+
expires: 2010-11-20
|
224
|
+
price: 133
|
225
|
+
stock: !ruby/object:Stock
|
226
|
+
price: 3734
|
227
|
+
symbol: JPM
|
228
|
+
strike: 3800
|
229
|
+
|
230
|
+
# HPQ: !ruby/object:CoveredCallPosition
|
231
|
+
# commission: OptionsHouseAlt
|
232
|
+
# num_shares: 300
|
233
|
+
# date_established: 2010-09-29
|
234
|
+
# option: !ruby/object:Call
|
235
|
+
# expires: 2010-10-16
|
236
|
+
# price: 179
|
237
|
+
# stock: !ruby/object:Stock
|
238
|
+
# price: 4221
|
239
|
+
# symbol: HPQ
|
240
|
+
# strike: 4100
|
241
|
+
#
|
242
|
+
# AET: !ruby/object:CoveredCallPosition
|
243
|
+
# commission: OptionsHouseAlt
|
244
|
+
# num_shares: 400
|
245
|
+
# date_established: 2010-09-29
|
246
|
+
# option: !ruby/object:Call
|
247
|
+
# expires: 2010-10-16
|
248
|
+
# price: 120
|
249
|
+
# stock: !ruby/object:Stock
|
250
|
+
# price: 3079
|
251
|
+
# symbol: AET
|
252
|
+
# strike: 3000
|
253
|
+
#
|
254
|
+
# MON: !ruby/object:CoveredCallPosition
|
255
|
+
# commission: OptionsHouseAlt
|
256
|
+
# num_shares: 200
|
257
|
+
# date_established: 2010-10-06
|
258
|
+
# option: !ruby/object:Call
|
259
|
+
# expires: 2010-10-16
|
260
|
+
# price: 212
|
261
|
+
# stock: !ruby/object:Stock
|
262
|
+
# price: 4891
|
263
|
+
# symbol: MON
|
264
|
+
# strike: 4750
|
265
|
+
|
266
|
+
PKX: !ruby/object:CoveredCallPosition
|
267
|
+
commission: OptionsHouseAlt
|
268
|
+
num_shares: 100
|
269
|
+
date_established: 2010-10-19
|
270
|
+
option: !ruby/object:Call
|
271
|
+
expires: 2010-11-20
|
272
|
+
price: 490
|
273
|
+
stock: !ruby/object:Stock
|
274
|
+
price: 10720
|
275
|
+
symbol: PKX
|
276
|
+
strike: 10500
|
277
|
+
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'option_calendar'
|
2
|
+
require 'test/unit'
|
3
|
+
|
4
|
+
class OptionCalendarTest < Test::Unit::TestCase
|
5
|
+
|
6
|
+
# Saturday expiries
|
7
|
+
EXPIRY_MONTHS = [
|
8
|
+
[Date.parse("Jan 1 2010"), Date.parse("Jan 16 2010")],
|
9
|
+
[Date.parse("Feb 1 2010"), Date.parse("Feb 20 2010")],
|
10
|
+
[Date.parse("Mar 1 2010"), Date.parse("Mar 20 2010")],
|
11
|
+
[Date.parse("Apr 1 2010"), Date.parse("Apr 17 2010")],
|
12
|
+
[Date.parse("May 1 2010"), Date.parse("May 22 2010")],
|
13
|
+
[Date.parse("Jun 1 2010"), Date.parse("Jun 19 2010")],
|
14
|
+
[Date.parse("Jul 1 2010"), Date.parse("Jul 17 2010")],
|
15
|
+
[Date.parse("Aug 1 2010"), Date.parse("Aug 21 2010")],
|
16
|
+
[Date.parse("Sep 1 2010"), Date.parse("Sep 18 2010")],
|
17
|
+
[Date.parse("Oct 1 2010"), Date.parse("Oct 16 2010")],
|
18
|
+
[Date.parse("Nov 1 2010"), Date.parse("Nov 20 2010")],
|
19
|
+
[Date.parse("Dec 1 2010"), Date.parse("Dec 18 2010")],
|
20
|
+
|
21
|
+
[Date.parse("Jan 1 2011"), Date.parse("Jan 22 2011")],
|
22
|
+
[Date.parse("Feb 1 2011"), Date.parse("Feb 19 2011")],
|
23
|
+
[Date.parse("Mar 1 2011"), Date.parse("Mar 19 2011")],
|
24
|
+
[Date.parse("Apr 1 2011"), Date.parse("Apr 16 2011")],
|
25
|
+
[Date.parse("May 1 2011"), Date.parse("May 21 2011")],
|
26
|
+
[Date.parse("Jun 1 2011"), Date.parse("Jun 18 2011")],
|
27
|
+
[Date.parse("Jul 1 2011"), Date.parse("Jul 16 2011")],
|
28
|
+
[Date.parse("Aug 1 2011"), Date.parse("Aug 20 2011")],
|
29
|
+
[Date.parse("Sep 1 2011"), Date.parse("Sep 17 2011")],
|
30
|
+
[Date.parse("Oct 1 2011"), Date.parse("Oct 22 2011")],
|
31
|
+
[Date.parse("Nov 1 2011"), Date.parse("Nov 19 2011")],
|
32
|
+
[Date.parse("Dec 1 2011"), Date.parse("Dec 17 2011")]
|
33
|
+
]
|
34
|
+
|
35
|
+
def test_monthly_expirations
|
36
|
+
EXPIRY_MONTHS.each do |month, exp|
|
37
|
+
message = "Month #{month.strftime("%b")} should have expiry of #{exp}"
|
38
|
+
|
39
|
+
assert_equal exp, OptionCalendar.expiration_for(month), message
|
40
|
+
assert_equal exp, OptionCalendar.expiration_for(month.end_of_month), message
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
NEXT_MONTHS = [
|
45
|
+
[Date.parse("Sep 15 2010"), Date.parse("Sep 18 2010"), Date.parse("Oct 16 2010")],
|
46
|
+
[Date.parse("Sep 16 2010"), Date.parse("Sep 18 2010"), Date.parse("Oct 16 2010")],
|
47
|
+
[Date.parse("Sep 17 2010"), Date.parse("Oct 16 2010"), Date.parse("Nov 20 2010")],
|
48
|
+
[Date.parse("Sep 18 2010"), Date.parse("Oct 16 2010"), Date.parse("Nov 20 2010")],
|
49
|
+
[Date.parse("Sep 19 2010"), Date.parse("Oct 16 2010"), Date.parse("Nov 20 2010")]
|
50
|
+
]
|
51
|
+
|
52
|
+
def test_next_expiring_months
|
53
|
+
NEXT_MONTHS.each do |date, *expected|
|
54
|
+
message = "#{date} should have expirations #{expected.inspect}"
|
55
|
+
|
56
|
+
assert_equal expected, OptionCalendar.next_expiring_months(date), message
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_nearest_expiration
|
61
|
+
jan_expiry = OptionCalendar.expiration_for(Date.parse("Jan 2011"))
|
62
|
+
feb_expiry = OptionCalendar.expiration_for(Date.parse("Feb 2011"))
|
63
|
+
|
64
|
+
assert_equal jan_expiry, OptionCalendar.nearest_expiration(jan_expiry - 4.days, 3.days)
|
65
|
+
assert_equal feb_expiry, OptionCalendar.nearest_expiration(jan_expiry - 3.days, 3.days)
|
66
|
+
assert_equal feb_expiry, OptionCalendar.nearest_expiration(jan_expiry - 2.days, 3.days)
|
67
|
+
|
68
|
+
assert_equal jan_expiry, OptionCalendar.nearest_expiration(Date.parse("Dec 31 2010"), 3.days)
|
69
|
+
assert_equal feb_expiry, OptionCalendar.nearest_expiration(Date.parse("Dec 31 2010"), 30.days)
|
70
|
+
assert_equal feb_expiry, OptionCalendar.nearest_expiration(Date.parse("Nov 30 2010"), 60.days)
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
@@ -0,0 +1,315 @@
|
|
1
|
+
require "test/unit"
|
2
|
+
require "flexmock/test_unit"
|
3
|
+
require "penfold"
|
4
|
+
|
5
|
+
class TestPenfold < Test::Unit::TestCase
|
6
|
+
|
7
|
+
# options
|
8
|
+
def test_call_type
|
9
|
+
call = Call.new
|
10
|
+
|
11
|
+
assert call.call?
|
12
|
+
assert !call.put?
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_put_type
|
16
|
+
put = Put.new
|
17
|
+
|
18
|
+
assert !put.call?
|
19
|
+
assert put.put?
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_option_cannot_be_instantiated
|
23
|
+
assert_raise ArgumentError do
|
24
|
+
Option.new
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_option_expiry
|
29
|
+
unexpired = Call.new(:expires => Date.today + 10)
|
30
|
+
expired = Call.new(:expires => Date.today - 10)
|
31
|
+
expires_today = Call.new(:expires => Date.today)
|
32
|
+
|
33
|
+
assert !unexpired.expired?
|
34
|
+
assert_equal 10, unexpired.days_to_expiry
|
35
|
+
|
36
|
+
assert expired.expired?
|
37
|
+
assert_equal 0, expired.days_to_expiry
|
38
|
+
|
39
|
+
assert !expires_today.expired?
|
40
|
+
assert_equal 0, expires_today.days_to_expiry
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_to_s
|
44
|
+
expires = Date.today + 93
|
45
|
+
|
46
|
+
stock = Stock.new(:symbol => "XYZ", :price => 50_00)
|
47
|
+
|
48
|
+
option = Call.new(
|
49
|
+
:stock => stock,
|
50
|
+
:strike => 35_00,
|
51
|
+
:expires => expires,
|
52
|
+
:price => 4_50
|
53
|
+
)
|
54
|
+
|
55
|
+
assert_equal "XYZ 35 #{expires.strftime("%b %y").upcase} CALL", option.to_s,
|
56
|
+
"A whole dollar amount does not require a floating point strike price"
|
57
|
+
|
58
|
+
option = Call.new(
|
59
|
+
:stock => stock,
|
60
|
+
:strike => 5_50,
|
61
|
+
:expires => expires,
|
62
|
+
:price => 4_50
|
63
|
+
)
|
64
|
+
|
65
|
+
assert_equal "XYZ 5.5 #{expires.strftime("%b %y").upcase} CALL", option.to_s,
|
66
|
+
"A partial dollar amount requires a floating point strike price"
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_out_the_money?
|
70
|
+
assert flexmock(Call.new, :in_the_money? => false).out_the_money?
|
71
|
+
assert !flexmock(Call.new, :in_the_money? => true).out_the_money?
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_call_moneyness
|
75
|
+
call = Call.new(:strike => 5_00, :stock => Stock.new(:price => 4_90))
|
76
|
+
|
77
|
+
assert !call.in_the_money?
|
78
|
+
assert !call.at_the_money?
|
79
|
+
|
80
|
+
call.stock.price += 10
|
81
|
+
assert !call.in_the_money?
|
82
|
+
assert call.at_the_money?
|
83
|
+
|
84
|
+
call.stock.price += 10
|
85
|
+
assert call.in_the_money?
|
86
|
+
assert !call.at_the_money?
|
87
|
+
end
|
88
|
+
|
89
|
+
def test_put_moneyness
|
90
|
+
put = Put.new(:strike => 5_00, :stock => Stock.new(:price => 4_90))
|
91
|
+
|
92
|
+
assert put.in_the_money?
|
93
|
+
assert !put.at_the_money?
|
94
|
+
|
95
|
+
put.stock.price += 10
|
96
|
+
assert !put.in_the_money?
|
97
|
+
assert put.at_the_money?
|
98
|
+
|
99
|
+
put.stock.price += 10
|
100
|
+
assert !put.in_the_money?
|
101
|
+
assert !put.at_the_money?
|
102
|
+
end
|
103
|
+
|
104
|
+
# position
|
105
|
+
|
106
|
+
def test_stock_total
|
107
|
+
position = CoveredCallPosition.new(
|
108
|
+
:num_shares => 500,
|
109
|
+
:option => Call.new(:stock => Stock.new(:price => 50_00)),
|
110
|
+
:commission => "OptionsHouse"
|
111
|
+
)
|
112
|
+
|
113
|
+
assert_equal 500 * 50_00 + position.commission.stock_entry, position.stock_total
|
114
|
+
end
|
115
|
+
|
116
|
+
def test_call_sale
|
117
|
+
position = CoveredCallPosition.new(
|
118
|
+
:num_shares => 500,
|
119
|
+
:option => Call.new(:price => 6_00),
|
120
|
+
:commission => "OptionsHouse"
|
121
|
+
)
|
122
|
+
|
123
|
+
assert_equal 3_000_00 - (position.commission.option_entry), position.call_sale
|
124
|
+
end
|
125
|
+
|
126
|
+
def test_net_outlay
|
127
|
+
position = flexmock(
|
128
|
+
CoveredCallPosition.new,
|
129
|
+
:stock_total => 25_000_00,
|
130
|
+
:call_sale => 3_000_00
|
131
|
+
)
|
132
|
+
|
133
|
+
assert_equal 22_000_00, position.net_outlay
|
134
|
+
end
|
135
|
+
|
136
|
+
def test_net_per_share
|
137
|
+
position = CoveredCallPosition.new(:num_shares => 500)
|
138
|
+
flexmock(position, :net_outlay => 22_012_20)
|
139
|
+
|
140
|
+
assert_equal 44_02.44, position.net_per_share
|
141
|
+
end
|
142
|
+
|
143
|
+
def test_downside_protection
|
144
|
+
position = flexmock(
|
145
|
+
CoveredCallPosition.new,
|
146
|
+
:net_per_share => 44_00,
|
147
|
+
:stock => Stock.new(:price => 50_00)
|
148
|
+
)
|
149
|
+
|
150
|
+
assert_equal(-0.12, position.downside_protection, "should be -12%")
|
151
|
+
end
|
152
|
+
|
153
|
+
def test_odd_lots
|
154
|
+
assert_raise ArgumentError do
|
155
|
+
CoveredCallPosition.new(:num_shares => 23)
|
156
|
+
end
|
157
|
+
|
158
|
+
assert_nothing_raised do
|
159
|
+
CoveredCallPosition.new(:num_shares => 100)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def test_num_options
|
164
|
+
assert 3, CoveredCallPosition.new(:num_shares => 300)
|
165
|
+
end
|
166
|
+
|
167
|
+
def test_stock
|
168
|
+
stock = Stock.new
|
169
|
+
position = CoveredCallPosition.new(:option => Put.new(:stock => stock))
|
170
|
+
|
171
|
+
assert_equal stock, position.stock
|
172
|
+
end
|
173
|
+
|
174
|
+
# exit
|
175
|
+
|
176
|
+
def test_creation
|
177
|
+
date = Date.today
|
178
|
+
|
179
|
+
position = CoveredCallPosition.new(
|
180
|
+
:num_shares => 500,
|
181
|
+
:commission => "OptionsHouse",
|
182
|
+
:date_established => date,
|
183
|
+
|
184
|
+
:option => Call.new(
|
185
|
+
:expires => date + 30,
|
186
|
+
:strike => 50_00,
|
187
|
+
:price => 6_00,
|
188
|
+
:stock => Stock.new(
|
189
|
+
:symbol => "XYZ",
|
190
|
+
:price => 50_00
|
191
|
+
)
|
192
|
+
)
|
193
|
+
)
|
194
|
+
|
195
|
+
closing_position = CoveredCallExit.new(
|
196
|
+
:opening_position => position,
|
197
|
+
:exit_date => date + 34,
|
198
|
+
:stock_price => 55_00
|
199
|
+
)
|
200
|
+
|
201
|
+
assert_not_same position.stock, closing_position.stock
|
202
|
+
assert_equal position.stock, closing_position.stock
|
203
|
+
|
204
|
+
assert_not_same position.option, closing_position.option
|
205
|
+
assert_equal position.option, closing_position.option
|
206
|
+
|
207
|
+
assert_same position.commission, closing_position.commission
|
208
|
+
|
209
|
+
assert_equal date + 30, closing_position.exit_date
|
210
|
+
assert closing_position.option.expired?
|
211
|
+
|
212
|
+
assert_equal 55_00, closing_position.stock.price
|
213
|
+
end
|
214
|
+
|
215
|
+
def test_creates_expired_position_when_expired_in_the_money
|
216
|
+
position = CoveredCallPosition.new(
|
217
|
+
:option => Call.new(
|
218
|
+
:stock => Stock.new(:price => 48_00),
|
219
|
+
:strike => 50_00,
|
220
|
+
:expires => Date.today + 10
|
221
|
+
)
|
222
|
+
)
|
223
|
+
|
224
|
+
expired_itm = CoveredCallExit.new(
|
225
|
+
:opening_position => position,
|
226
|
+
:stock_price => 52_00,
|
227
|
+
:exit_date => Date.today + 11
|
228
|
+
)
|
229
|
+
|
230
|
+
expired_otm = CoveredCallExit.new(
|
231
|
+
:opening_position => position,
|
232
|
+
:stock_price => 48_00,
|
233
|
+
:exit_date => Date.today + 11
|
234
|
+
)
|
235
|
+
|
236
|
+
unexpired_itm = CoveredCallExit.new(
|
237
|
+
:opening_position => position,
|
238
|
+
:stock_price => 52_00,
|
239
|
+
:exit_date => Date.today + 1
|
240
|
+
)
|
241
|
+
|
242
|
+
unexpired_otm = CoveredCallExit.new(
|
243
|
+
:opening_position => position,
|
244
|
+
:stock_price => 48_00,
|
245
|
+
:exit_date => Date.today + 1
|
246
|
+
)
|
247
|
+
|
248
|
+
assert_kind_of CoveredCallExpiryItmExit, expired_itm
|
249
|
+
assert !expired_otm.kind_of?(CoveredCallExpiryItmExit)
|
250
|
+
assert !unexpired_itm.kind_of?(CoveredCallExpiryItmExit)
|
251
|
+
assert !unexpired_otm.kind_of?(CoveredCallExpiryItmExit)
|
252
|
+
end
|
253
|
+
|
254
|
+
def test_no_profit_method_body_for_default_class
|
255
|
+
assert_raise NotImplementedError do
|
256
|
+
CoveredCallExit.allocate.profit
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
# Option pricing
|
261
|
+
|
262
|
+
def test_black_scholes
|
263
|
+
assert_in_delta 0.35725, BlackScholes.call_iv(19.18, 17.50, 0.27, 32, 1.89), 0.0001
|
264
|
+
assert_in_delta 0.25866, BlackScholes.call_iv(19.18, 19, 0.27, 32, 0.68), 0.0001
|
265
|
+
end
|
266
|
+
|
267
|
+
def test_probability
|
268
|
+
vol = BlackScholes.call_iv(19.18, 19, 0.27, 32, 0.68)
|
269
|
+
|
270
|
+
below, above = BlackScholes.probability(19.18, 19, 32, vol)
|
271
|
+
|
272
|
+
assert_in_delta 0.548, above, 0.001
|
273
|
+
assert_in_delta 0.451, below, 0.001
|
274
|
+
|
275
|
+
assert_equal above, BlackScholes.probability_above(19.18, 19, 32, vol)
|
276
|
+
assert_equal below, BlackScholes.probability_below(19.18, 19, 32, vol)
|
277
|
+
end
|
278
|
+
|
279
|
+
# comms
|
280
|
+
|
281
|
+
def test_optionshouse
|
282
|
+
(0..10).each do |i|
|
283
|
+
cost = 8_50 + (i * 15)
|
284
|
+
|
285
|
+
oh = Commission::OptionsHouse.new(:contracts => i)
|
286
|
+
assert_equal i.zero? ? 0 : cost, oh.option_entry
|
287
|
+
end
|
288
|
+
|
289
|
+
oh = Commission::OptionsHouse.new(:shares => 0)
|
290
|
+
assert_equal 0, oh.stock_entry
|
291
|
+
|
292
|
+
oh = Commission::OptionsHouse.new(:shares => 123)
|
293
|
+
assert_equal 2_95, oh.stock_entry
|
294
|
+
|
295
|
+
oh = Commission::OptionsHouse.new
|
296
|
+
assert_equal 0, oh.stock_entry
|
297
|
+
assert_equal 0, oh.option_entry
|
298
|
+
assert_equal 0, oh.option_assignment
|
299
|
+
end
|
300
|
+
|
301
|
+
def test_optionshouse_alt
|
302
|
+
opt_comms = [
|
303
|
+
[0, 0],
|
304
|
+
[4, 5_00],
|
305
|
+
[5, 5_00],
|
306
|
+
[6, 6_00]
|
307
|
+
]
|
308
|
+
|
309
|
+
opt_comms.each do |num_contracts, cost|
|
310
|
+
oha = Commission::OptionsHouseAlt.new(:shares => 0, :contracts => num_contracts)
|
311
|
+
assert_equal cost, oha.option_entry
|
312
|
+
assert_equal num_contracts.zero? ? 0 : 5_00, oha.option_assignment
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|