wco_models 3.1.0.187 → 3.1.0.189
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/models/iro/alert.rb +63 -0
- data/app/models/iro/datapoint.rb +187 -0
- data/app/models/iro/date.rb +10 -0
- data/app/models/iro/option.rb +154 -0
- data/app/models/iro/option_black_scholes.rb +149 -0
- data/app/models/iro/position.rb +304 -0
- data/app/models/iro/priceitem.rb +93 -0
- data/app/models/iro/purse.rb +71 -0
- data/app/models/iro/stock.rb +141 -0
- data/app/models/iro/strategy.rb +284 -0
- data/app/models/wco/profile.rb +3 -0
- data/app/models/wco_email/message_stub.rb +10 -1
- data/app/views/wco/profiles/_form.haml +10 -0
- metadata +12 -2
@@ -0,0 +1,304 @@
|
|
1
|
+
|
2
|
+
class Iro::Position
|
3
|
+
include Mongoid::Document
|
4
|
+
include Mongoid::Timestamps
|
5
|
+
include Mongoid::Paranoia
|
6
|
+
store_in collection: 'iro_positions'
|
7
|
+
|
8
|
+
field :prev_gain_loss_amount, type: :float
|
9
|
+
attr_accessor :next_gain_loss_amount
|
10
|
+
def prev_gain_loss_amount
|
11
|
+
out = autoprev.outer.end_price - autoprev.inner.end_price
|
12
|
+
out += inner.begin_price - outer.begin_price
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
STATUS_ACTIVE = 'active'
|
17
|
+
STATUS_CLOSED = 'closed'
|
18
|
+
STATUS_PROPOSED = 'proposed'
|
19
|
+
## one more, 'selected' after proposed?
|
20
|
+
STATUS_PENDING = 'pending' ## 'working'
|
21
|
+
STATUSES = [ nil, STATUS_CLOSED, STATUS_ACTIVE, STATUS_PROPOSED, STATUS_PENDING ]
|
22
|
+
field :status
|
23
|
+
validates :status, presence: true
|
24
|
+
scope :active, ->{ where( status: 'active' ) }
|
25
|
+
|
26
|
+
belongs_to :purse, class_name: 'Iro::Purse', inverse_of: :positions
|
27
|
+
index({ purse_id: 1, ticker: 1 })
|
28
|
+
|
29
|
+
belongs_to :stock, class_name: 'Iro::Stock', inverse_of: :positions
|
30
|
+
delegate :ticker, to: :stock
|
31
|
+
|
32
|
+
belongs_to :strategy, class_name: 'Iro::Strategy', inverse_of: :positions
|
33
|
+
delegate :put_call, to: :strategy
|
34
|
+
delegate :long_or_short, to: :strategy
|
35
|
+
delegate :credit_or_debit, to: :strategy
|
36
|
+
|
37
|
+
belongs_to :next_strategy, class_name: 'Iro::Strategy', inverse_of: :next_position, optional: true
|
38
|
+
|
39
|
+
|
40
|
+
belongs_to :prev, class_name: 'Iro::Position', inverse_of: :nxts, optional: true
|
41
|
+
belongs_to :autoprev, class_name: 'Iro::Position', inverse_of: :autonxt, optional: true
|
42
|
+
## there are many of these, for viewing on the 'roll' view
|
43
|
+
has_many :nxts, class_name: 'Iro::Position', inverse_of: :prev
|
44
|
+
has_one :autonxt, class_name: 'Iro::Position', inverse_of: :autoprev
|
45
|
+
|
46
|
+
## Options
|
47
|
+
|
48
|
+
belongs_to :inner, class_name: 'Iro::Option', inverse_of: :inner
|
49
|
+
validates_associated :inner
|
50
|
+
|
51
|
+
belongs_to :outer, class_name: 'Iro::Option', inverse_of: :outer
|
52
|
+
validates_associated :outer
|
53
|
+
|
54
|
+
accepts_nested_attributes_for :inner, :outer
|
55
|
+
|
56
|
+
field :outer_strike, type: :float
|
57
|
+
# validates :outer_strike, presence: true
|
58
|
+
|
59
|
+
field :inner_strike, type: :float
|
60
|
+
# validates :inner_strike, presence: true
|
61
|
+
|
62
|
+
field :expires_on
|
63
|
+
validates :expires_on, presence: true
|
64
|
+
|
65
|
+
field :quantity, type: :integer
|
66
|
+
validates :quantity, presence: true
|
67
|
+
def q; quantity; end
|
68
|
+
|
69
|
+
field :begin_on
|
70
|
+
|
71
|
+
field :end_on
|
72
|
+
|
73
|
+
def begin_delta
|
74
|
+
strategy.send("begin_delta_#{strategy.kind}", self)
|
75
|
+
end
|
76
|
+
def end_delta
|
77
|
+
strategy.send("end_delta_#{strategy.kind}", self)
|
78
|
+
end
|
79
|
+
|
80
|
+
def breakeven
|
81
|
+
strategy.send("breakeven_#{strategy.kind}", self)
|
82
|
+
end
|
83
|
+
|
84
|
+
def current_underlying_strike
|
85
|
+
Iro::Stock.find_by( ticker: ticker ).last
|
86
|
+
end
|
87
|
+
|
88
|
+
def refresh
|
89
|
+
out = Tda::Option.get_quote({
|
90
|
+
contractType: 'CALL',
|
91
|
+
strike: strike,
|
92
|
+
expirationDate: expires_on,
|
93
|
+
ticker: ticker,
|
94
|
+
})
|
95
|
+
update({
|
96
|
+
end_delta: out[:delta],
|
97
|
+
end_price: out[:last],
|
98
|
+
})
|
99
|
+
print '^'
|
100
|
+
end
|
101
|
+
|
102
|
+
def net_percent
|
103
|
+
net_amount / max_gain
|
104
|
+
end
|
105
|
+
def net_amount # each
|
106
|
+
strategy.send("net_amount_#{strategy.kind}", self)
|
107
|
+
end
|
108
|
+
def max_gain # each
|
109
|
+
strategy.send("max_gain_#{strategy.kind}", self)
|
110
|
+
end
|
111
|
+
def max_loss # each
|
112
|
+
strategy.send("max_loss_#{strategy.kind}", self)
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
def sync
|
117
|
+
inner.sync
|
118
|
+
outer.sync
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
##
|
123
|
+
## decisions
|
124
|
+
##
|
125
|
+
|
126
|
+
field :next_reasons, type: :array, default: []
|
127
|
+
field :rollp, type: :float
|
128
|
+
|
129
|
+
## should_roll?
|
130
|
+
def calc_rollp
|
131
|
+
self.next_reasons = []
|
132
|
+
# self.next_symbol = nil
|
133
|
+
# self.next_delta = nil
|
134
|
+
|
135
|
+
out = strategy.send( "calc_rollp_#{strategy.kind}", self )
|
136
|
+
|
137
|
+
self.rollp = out[0]
|
138
|
+
self.next_reasons.push out[1]
|
139
|
+
save
|
140
|
+
end
|
141
|
+
|
142
|
+
def calc_nxt
|
143
|
+
pos = self
|
144
|
+
|
145
|
+
## 7 days ahead - not configurable so far
|
146
|
+
outs = Tda::Option.get_quotes({
|
147
|
+
contractType: pos.put_call,
|
148
|
+
expirationDate: next_expires_on,
|
149
|
+
ticker: ticker,
|
150
|
+
})
|
151
|
+
outs_bk = outs.dup
|
152
|
+
|
153
|
+
outs = outs.select do |out|
|
154
|
+
out[:bidSize] + out[:askSize] > 0
|
155
|
+
end
|
156
|
+
|
157
|
+
if 'CALL' == pos.put_call
|
158
|
+
;
|
159
|
+
elsif 'PUT' == pos.put_call
|
160
|
+
outs = outs.reverse
|
161
|
+
end
|
162
|
+
|
163
|
+
## next_inner_strike
|
164
|
+
outs = outs.select do |out|
|
165
|
+
if Iro::Strategy::CREDIT == pos.credit_or_debit
|
166
|
+
if Iro::Strategy::SHORT == pos.long_or_short
|
167
|
+
## short credit call
|
168
|
+
out[:strikePrice] >= strategy.next_inner_strike
|
169
|
+
elsif Iro::Strategy::LONG == pos.long_or_short
|
170
|
+
## long credit put
|
171
|
+
out[:strikePrice] <= strategy.next_inner_strike
|
172
|
+
end
|
173
|
+
else
|
174
|
+
raise 'zz3 - @TODO: implement, debit spreads'
|
175
|
+
end
|
176
|
+
end
|
177
|
+
puts! outs[0][:strikePrice], 'after calc next_inner_strike'
|
178
|
+
puts! outs, 'outs'
|
179
|
+
|
180
|
+
## next_buffer_above_water
|
181
|
+
outs = outs.select do |out|
|
182
|
+
if Iro::Strategy::SHORT == pos.long_or_short
|
183
|
+
out[:strikePrice] > strategy.next_buffer_above_water + strategy.stock.last
|
184
|
+
elsif Iro::Strategy::LONG == pos.long_or_short
|
185
|
+
out[:strikePrice] < strategy.stock.last - strategy.next_buffer_above_water
|
186
|
+
else
|
187
|
+
raise 'zz4 - this cannot happen'
|
188
|
+
end
|
189
|
+
end
|
190
|
+
puts! outs[0][:strikePrice], 'after calc next_buffer_above_water'
|
191
|
+
puts! outs, 'outs'
|
192
|
+
|
193
|
+
## next_inner_delta
|
194
|
+
outs = outs.select do |out|
|
195
|
+
if 'CALL' == pos.put_call
|
196
|
+
out_delta = out[:delta] rescue 1
|
197
|
+
out_delta <= strategy.next_inner_delta
|
198
|
+
elsif 'PUT' == pos.put_call
|
199
|
+
out_delta = out[:delta] rescue 0
|
200
|
+
out_delta <= strategy.next_inner_delta
|
201
|
+
else
|
202
|
+
raise 'zz5 - this cannot happen'
|
203
|
+
end
|
204
|
+
end
|
205
|
+
puts! outs[0][:strikePrice], 'after calc next_inner_delta'
|
206
|
+
puts! outs, 'outs'
|
207
|
+
|
208
|
+
inner = outs[0]
|
209
|
+
outs = outs.select do |out|
|
210
|
+
if 'CALL' == pos.put_call
|
211
|
+
out[:strikePrice] >= inner[:strikePrice].to_f + strategy.next_spread_amount
|
212
|
+
elsif 'PUT' == pos.put_call
|
213
|
+
out[:strikePrice] <= inner[:strikePrice].to_f - strategy.next_spread_amount
|
214
|
+
end
|
215
|
+
end
|
216
|
+
outer = outs[0]
|
217
|
+
|
218
|
+
if inner && outer
|
219
|
+
o_attrs = {
|
220
|
+
expires_on: next_expires_on,
|
221
|
+
put_call: pos.put_call,
|
222
|
+
stock_id: pos.stock_id,
|
223
|
+
}
|
224
|
+
inner_ = Iro::Option.new(o_attrs.merge({
|
225
|
+
strike: inner[:strikePrice],
|
226
|
+
begin_price: ( inner[:bid] + inner[:ask] )/2,
|
227
|
+
begin_delta: inner[:delta],
|
228
|
+
end_price: ( inner[:bid] + inner[:ask] )/2,
|
229
|
+
end_delta: inner[:delta],
|
230
|
+
}))
|
231
|
+
outer_ = Iro::Option.new(o_attrs.merge({
|
232
|
+
strike: outer[:strikePrice],
|
233
|
+
begin_price: ( outer[:bid] + outer[:ask] )/2,
|
234
|
+
begin_delta: outer[:delta],
|
235
|
+
end_price: ( outer[:bid] + outer[:ask] )/2,
|
236
|
+
end_delta: outer[:delta],
|
237
|
+
}))
|
238
|
+
pos.autonxt ||= Iro::Position.new
|
239
|
+
pos.autonxt.update({
|
240
|
+
prev_gain_loss_amount: 'a',
|
241
|
+
status: 'proposed',
|
242
|
+
stock: strategy.stock,
|
243
|
+
inner: inner_,
|
244
|
+
outer: outer_,
|
245
|
+
inner_strike: inner_.strike,
|
246
|
+
outer_strike: outer_.strike,
|
247
|
+
begin_on: Time.now.to_date,
|
248
|
+
expires_on: next_expires_on,
|
249
|
+
purse: purse,
|
250
|
+
strategy: strategy,
|
251
|
+
quantity: 1,
|
252
|
+
autoprev: pos,
|
253
|
+
})
|
254
|
+
|
255
|
+
pos.autonxt.sync
|
256
|
+
pos.autonxt.save!
|
257
|
+
pos.save
|
258
|
+
return pos
|
259
|
+
|
260
|
+
else
|
261
|
+
throw 'zmq - should not happen'
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
|
266
|
+
|
267
|
+
## ok
|
268
|
+
def next_expires_on
|
269
|
+
out = expires_on.to_datetime.next_occurring(:monday).next_occurring(:friday)
|
270
|
+
if !out.workday?
|
271
|
+
out = Time.previous_business_day(out)
|
272
|
+
end
|
273
|
+
return out
|
274
|
+
end
|
275
|
+
|
276
|
+
## ok
|
277
|
+
def self.long
|
278
|
+
where( long_or_short: Iro::Strategy::LONG )
|
279
|
+
end
|
280
|
+
|
281
|
+
## ok
|
282
|
+
def self.short
|
283
|
+
where( long_or_short: Iro::Strategy::SHORT )
|
284
|
+
end
|
285
|
+
|
286
|
+
def to_s
|
287
|
+
out = "#{stock} (#{q}) #{expires_on.to_datetime.strftime('%b %d')} #{strategy.long_or_short} ["
|
288
|
+
if Iro::Strategy::LONG == long_or_short
|
289
|
+
if outer.strike
|
290
|
+
out = out + "$#{outer.strike}->"
|
291
|
+
end
|
292
|
+
out = out + "$#{inner.strike}"
|
293
|
+
else
|
294
|
+
out = out + "$#{inner.strike}"
|
295
|
+
if outer.strike
|
296
|
+
out = out + "<-$#{outer.strike}"
|
297
|
+
end
|
298
|
+
end
|
299
|
+
out += "] "
|
300
|
+
return out
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
|
@@ -0,0 +1,93 @@
|
|
1
|
+
|
2
|
+
##
|
3
|
+
## Specifically Option or Stock priceitem?
|
4
|
+
## Priceitems are intra-day! See Datapoint for daily data
|
5
|
+
##
|
6
|
+
class Iro::Priceitem
|
7
|
+
include Mongoid::Document
|
8
|
+
include Mongoid::Timestamps
|
9
|
+
store_in collection: 'iro_price_items'
|
10
|
+
|
11
|
+
## PUT, CALL, STOCK
|
12
|
+
field :putCall, type: String ## kind
|
13
|
+
field :symbol, type: String
|
14
|
+
field :description, type: String
|
15
|
+
field :ticker, type: String
|
16
|
+
# belongs_to :stock, inverse_of: :priceitems
|
17
|
+
|
18
|
+
field :bid, type: Float
|
19
|
+
field :bidSize, type: Integer
|
20
|
+
field :ask, type: Float
|
21
|
+
field :askSize, type: Integer
|
22
|
+
field :last, type: Float
|
23
|
+
|
24
|
+
field :openPrice, type: Float
|
25
|
+
field :lowPrice, type: Float
|
26
|
+
field :highPrice, type: Float
|
27
|
+
field :closePrice, type: Float
|
28
|
+
|
29
|
+
field :quote_at, type: DateTime
|
30
|
+
field :quoteTimeInLong, type: Integer
|
31
|
+
field :timestamp, type: Integer
|
32
|
+
field :totalVolume, type: Integer
|
33
|
+
field :mark, type: Float
|
34
|
+
field :exchangeName, type: String
|
35
|
+
field :volatility, type: Float
|
36
|
+
|
37
|
+
field :expirationDate, type: :date
|
38
|
+
field :delta, type: Float
|
39
|
+
field :gamma, type: Float
|
40
|
+
field :theta, type: Float
|
41
|
+
field :openInterest, type: Integer
|
42
|
+
field :strikePrice, type: Float
|
43
|
+
|
44
|
+
def self.my_find props={}
|
45
|
+
lookup = { '$lookup': {
|
46
|
+
'from': 'iro_price_items',
|
47
|
+
'localField': 'date',
|
48
|
+
'foreignField': 'date',
|
49
|
+
'pipeline': [
|
50
|
+
{ '$sort': { 'value': -1 } },
|
51
|
+
],
|
52
|
+
'as': 'dates',
|
53
|
+
} }
|
54
|
+
lookup_merge = { '$replaceRoot': {
|
55
|
+
'newRoot': { '$mergeObjects': [
|
56
|
+
{ '$arrayElemAt': [ "$dates", 0 ] }, "$$ROOT"
|
57
|
+
] }
|
58
|
+
} }
|
59
|
+
|
60
|
+
|
61
|
+
match = { '$match': {
|
62
|
+
'date': {
|
63
|
+
'$gte': props[:begin_on],
|
64
|
+
'$lte': props[:end_on],
|
65
|
+
}
|
66
|
+
} }
|
67
|
+
|
68
|
+
group = { '$group': {
|
69
|
+
'_id': "$date",
|
70
|
+
'my_doc': { '$first': "$$ROOT" }
|
71
|
+
} }
|
72
|
+
|
73
|
+
outs = Iro::Date.collection.aggregate([
|
74
|
+
match,
|
75
|
+
|
76
|
+
lookup,
|
77
|
+
lookup_merge,
|
78
|
+
|
79
|
+
group,
|
80
|
+
{ '$replaceRoot': { 'newRoot': "$my_doc" } },
|
81
|
+
# { '$replaceRoot': { 'newRoot': "$my_doc" } },
|
82
|
+
|
83
|
+
|
84
|
+
{ '$project': { '_id': 0, 'date': 1, 'value': 1 } },
|
85
|
+
{ '$sort': { 'date': 1 } },
|
86
|
+
])
|
87
|
+
|
88
|
+
puts! 'result'
|
89
|
+
pp outs.to_a
|
90
|
+
# puts! outs.to_a, 'result'
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
|
2
|
+
require 'distribution'
|
3
|
+
N = Distribution::Normal
|
4
|
+
|
5
|
+
class Iro::Purse
|
6
|
+
include Mongoid::Document
|
7
|
+
include Mongoid::Timestamps
|
8
|
+
include Mongoid::Paranoia
|
9
|
+
store_in collection: 'iro_purses'
|
10
|
+
|
11
|
+
field :slug
|
12
|
+
validates :slug, presence: true, uniqueness: true
|
13
|
+
index({ slug: -1 }, { unique: true })
|
14
|
+
|
15
|
+
has_many :positions, class_name: 'Iro::Position', inverse_of: :purse
|
16
|
+
|
17
|
+
# has_and_belongs_to_many :strategies, class_name: 'Iro::Strategy', inverse_of: :purses
|
18
|
+
# belongs_to :stock, class_name: 'Iro::Stock', inverse_of: :strategies
|
19
|
+
|
20
|
+
field :unit, type: :integer, default: 10
|
21
|
+
## with unit 10, .001
|
22
|
+
## with unit 100, .0001
|
23
|
+
field :summary_unit, type: :float, default: 0.001
|
24
|
+
|
25
|
+
## for rolling only:
|
26
|
+
field :height, type: :integer, default: 100
|
27
|
+
|
28
|
+
field :mark_every_n_usd, type: :float, default: 1
|
29
|
+
field :n_next_positions, type: :integer, default: 5
|
30
|
+
|
31
|
+
field :available_amount, type: :float
|
32
|
+
def available
|
33
|
+
available_amount
|
34
|
+
end
|
35
|
+
|
36
|
+
def balance
|
37
|
+
0.01
|
38
|
+
end
|
39
|
+
|
40
|
+
def delta_wt_avg( begin_end, long_short, inner_outer )
|
41
|
+
max_loss_total = 0
|
42
|
+
|
43
|
+
out = positions.send( long_short ).map do |pos|
|
44
|
+
max_loss_total += pos.max_loss * pos.q
|
45
|
+
pos.max_loss * pos.q * pos.send( inner_outer ).send( "#{begin_end}_delta" )
|
46
|
+
end
|
47
|
+
# puts! out, 'delta_wt_avg 1'
|
48
|
+
out = out.reduce( &:+ ) / max_loss_total rescue 0
|
49
|
+
# puts! out, 'delta_wt_avg 2'
|
50
|
+
return out
|
51
|
+
end
|
52
|
+
## delta to plot percentage
|
53
|
+
## convert to normal between 0 and 3 std
|
54
|
+
def delta_to_plot_p( *args )
|
55
|
+
x = delta_wt_avg( *args ).abs
|
56
|
+
if x < 0.5
|
57
|
+
y = 1
|
58
|
+
else
|
59
|
+
y = 2 - 1/( 1.5 - x )
|
60
|
+
end
|
61
|
+
y_ = "#{ (y*100) .to_i}%"
|
62
|
+
return y_
|
63
|
+
end
|
64
|
+
|
65
|
+
def to_s
|
66
|
+
slug
|
67
|
+
end
|
68
|
+
def self.list
|
69
|
+
[[nil,nil]] + all.map { |p| [p, p.id] }
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
include Math
|
2
|
+
require 'business_time'
|
3
|
+
|
4
|
+
##
|
5
|
+
## https://www.macrotrends.net/stocks/charts/META/meta-platforms/stock-price-history
|
6
|
+
##
|
7
|
+
class Iro::Stock
|
8
|
+
include Mongoid::Document
|
9
|
+
include Mongoid::Timestamps
|
10
|
+
include Mongoid::Paranoia
|
11
|
+
store_in collection: 'iro_stocks'
|
12
|
+
|
13
|
+
STATUS_ACTIVE = 'active'
|
14
|
+
STATUS_INACTIVE = 'inactive'
|
15
|
+
STATUSES = [ nil, 'active', 'inactive' ]
|
16
|
+
def self.active
|
17
|
+
where( status: STATUS_ACTIVE )
|
18
|
+
end
|
19
|
+
field :status, default: STATUS_ACTIVE
|
20
|
+
|
21
|
+
field :ticker
|
22
|
+
validates :ticker, uniqueness: true, presence: true
|
23
|
+
index({ ticker: -1 }, { unique: true })
|
24
|
+
def symbol; ticker; end
|
25
|
+
def symbol= a; ticker = a; end
|
26
|
+
|
27
|
+
field :last, type: :float
|
28
|
+
field :options_price_increment, type: :float
|
29
|
+
|
30
|
+
field :stdev, type: :float
|
31
|
+
|
32
|
+
has_many :positions, class_name: 'Iro::Position', inverse_of: :stock
|
33
|
+
has_many :strategies, class_name: 'Iro::Strategy', inverse_of: :stock
|
34
|
+
# has_many :purses, class_name: 'Iro::Purse', inverse_of: :stock
|
35
|
+
has_many :options, class_name: 'Iro::Option', inverse_of: :stock
|
36
|
+
has_many :priceitems, inverse_of: :stock
|
37
|
+
|
38
|
+
belongs_to :user
|
39
|
+
LONG_ONLY = 'long-only'
|
40
|
+
LONG_OR_SHORT = 'long-or-short'
|
41
|
+
SHORT_ONLY = 'short-only'
|
42
|
+
field :sentiment, default: LONG_OR_SHORT
|
43
|
+
field :sentiment_num, default: 0 # 1 is very long, -1 is very short
|
44
|
+
|
45
|
+
default_scope { order_by({ ticker: :asc }) }
|
46
|
+
|
47
|
+
## my_find
|
48
|
+
def self.f ticker
|
49
|
+
self.find_by ticker: ticker
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_s
|
53
|
+
ticker
|
54
|
+
end
|
55
|
+
def self.list
|
56
|
+
[[nil,nil]] + all.map { |sss| [ sss.ticker, sss.id ] }
|
57
|
+
end
|
58
|
+
def self.tickers_list
|
59
|
+
[[nil,nil]] + all.map { |sss| [ sss.ticker, sss.ticker ] }
|
60
|
+
end
|
61
|
+
|
62
|
+
=begin
|
63
|
+
stock = Iro::Stock.find_by( ticker: 'NVDA' )
|
64
|
+
|
65
|
+
duration = 1.month
|
66
|
+
stock.volatility_from_mo
|
67
|
+
|
68
|
+
duration = 1.year
|
69
|
+
stock.volatility_from_yr
|
70
|
+
|
71
|
+
=end
|
72
|
+
field :volatility, type: :float
|
73
|
+
def volatility duration: 1.year, recompute: false
|
74
|
+
if self[:volatility]
|
75
|
+
if !recompute
|
76
|
+
return self[:volatility]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
stock = self
|
81
|
+
begin_on = Time.now - duration - 1.day
|
82
|
+
points = Iro::Datapoint.where( kind: 'STOCK', symbol: stock.ticker,
|
83
|
+
:date.gte => begin_on,
|
84
|
+
).order_by( date: :asc )
|
85
|
+
|
86
|
+
puts! [points.first.date, points.last.date], "from,to"
|
87
|
+
|
88
|
+
points_p = []
|
89
|
+
points.each_with_index do |p, idx|
|
90
|
+
next if idx == 0
|
91
|
+
prev = points[idx-1]
|
92
|
+
|
93
|
+
out = p.value / prev.value - 1
|
94
|
+
points_p.push out
|
95
|
+
end
|
96
|
+
n = points_p.length
|
97
|
+
|
98
|
+
avg = points_p.reduce(&:+) / n
|
99
|
+
_sum_of_sq = []
|
100
|
+
points_p.map do |p|
|
101
|
+
_sum_of_sq.push( ( p - avg )*( p - avg ) )
|
102
|
+
end
|
103
|
+
sum_of_sq = _sum_of_sq.reduce( &:+ ) / n
|
104
|
+
|
105
|
+
# n_periods = begin_on.to_date.business_days_until( Date.today )
|
106
|
+
out = Math.sqrt( sum_of_sq )*sqrt( n )
|
107
|
+
adjustment = 2.0
|
108
|
+
out = out * adjustment
|
109
|
+
puts! out, 'volatility (adjusted)'
|
110
|
+
self.update volatility: out
|
111
|
+
return out
|
112
|
+
end
|
113
|
+
|
114
|
+
def volatility_from_mo
|
115
|
+
volatility( duration: 1.month )
|
116
|
+
end
|
117
|
+
def volatility_from_yr
|
118
|
+
volatility( duration: 1.year )
|
119
|
+
end
|
120
|
+
def stdev recompute: nil
|
121
|
+
if !self[:stdev] || recompute
|
122
|
+
out = volatility_from_yr
|
123
|
+
self[:stdev] = out
|
124
|
+
save( validate: false )
|
125
|
+
return out
|
126
|
+
else
|
127
|
+
self[:stdev]
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
## stdev
|
132
|
+
## From: https://stackoverflow.com/questions/19484891/how-do-i-find-the-standard-deviation-in-ruby
|
133
|
+
# contents = [1,2,3,4,5,6,7,8,9]
|
134
|
+
# n = contents.size # => 9
|
135
|
+
# contents.map!(&:to_f) # => [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]
|
136
|
+
# mean = contents.reduce(&:+)/n # => 5.0
|
137
|
+
# sum_sqr = contents.map {|x| x * x}.reduce(&:+) # => 285.0
|
138
|
+
# std_dev = Math.sqrt((sum_sqr - n * mean * mean)/(n-1)) # => 2.7386127875258306
|
139
|
+
|
140
|
+
|
141
|
+
end
|