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
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
|