iron_warbler 2.0.7.25 → 2.0.7.26

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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/iron_warbler/Card.scss +6 -0
  3. data/app/assets/stylesheets/iron_warbler/positions.scss +1 -1
  4. data/app/assets/stylesheets/iron_warbler/purses_summary.scss +31 -19
  5. data/app/controllers/iro/application_controller.rb +6 -0
  6. data/app/controllers/iro/positions_controller.rb +183 -200
  7. data/app/controllers/iro/purses_controller.rb +3 -1
  8. data/app/controllers/iro/stocks_controller.rb +2 -2
  9. data/app/models/iro/option.rb +38 -150
  10. data/app/models/iro/option_black_scholes.rb +149 -0
  11. data/app/models/iro/position.rb +154 -209
  12. data/app/models/iro/purse.rb +34 -4
  13. data/app/models/iro/strategy.rb +49 -47
  14. data/app/views/iro/_main_header.haml +4 -2
  15. data/app/views/iro/options/_show_mini.haml +8 -0
  16. data/app/views/iro/positions/_form.haml +8 -3
  17. data/app/views/iro/positions/_formpart_4data.haml +41 -38
  18. data/app/views/iro/positions/_gameui_long_debit_call_spread.haml +4 -4
  19. data/app/views/iro/positions/_gameui_long_debit_call_spread.haml-trash +42 -0
  20. data/app/views/iro/positions/_gameui_short_debit_put_spread.haml +1 -0
  21. data/app/views/iro/positions/_gameui_short_debit_put_spread.haml-trash +40 -0
  22. data/app/views/iro/positions/_header.haml +2 -0
  23. data/app/views/iro/positions/_header_long_debit_call_spread.haml +43 -25
  24. data/app/views/iro/positions/_prepare_long_debit_call_spread.haml +2 -3
  25. data/app/views/iro/positions/_prepare_short_debit_put_spread.haml +2 -1
  26. data/app/views/iro/positions/_table.haml +25 -26
  27. data/app/views/iro/positions/prepare.haml +6 -4
  28. data/app/views/iro/positions/prepare2.haml +15 -4
  29. data/app/views/iro/purses/_form_extra_fields.haml +7 -3
  30. data/app/views/iro/purses/_header.haml +19 -5
  31. data/app/views/iro/purses/_summary.haml +69 -62
  32. data/app/views/iro/purses/show.haml +1 -1
  33. data/app/views/iro/strategies/_form.haml +26 -19
  34. data/app/views/iro/strategies/_show.haml +6 -4
  35. data/config/routes.rb +5 -3
  36. metadata +7 -2
  37. data/app/views/iro/positions/_gameui_short_debit_put_spread.haml +0 -40
@@ -5,11 +5,18 @@ class Iro::Position
5
5
  include Mongoid::Paranoia
6
6
  store_in collection: 'iro_positions'
7
7
 
8
+ field :prev_gain_loss_amount, type: :float
8
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
9
14
 
10
15
  STATUS_ACTIVE = 'active'
11
16
  STATUS_PROPOSED = 'proposed'
12
- STATUSES = [ nil, 'active', 'inactive', 'proposed' ]
17
+ STATUS_CLOSED = 'closed'
18
+ STATUS_PENDING = 'pending'
19
+ STATUSES = [ nil, STATUS_ACTIVE, STATUS_PROPOSED, STATUS_CLOSED, STATUS_PENDING ]
13
20
  field :status
14
21
  validates :status, presence: true
15
22
  scope :active, ->{ where( status: 'active' ) }
@@ -17,27 +24,40 @@ class Iro::Position
17
24
  belongs_to :purse, class_name: 'Iro::Purse', inverse_of: :positions
18
25
  index({ purse_id: 1, ticker: 1 })
19
26
 
20
- belongs_to :prev, class_name: 'Iro::Position', inverse_of: :nxt
21
- has_one :nxt, class_name: 'Iro::Position', inverse_of: :prev
22
-
23
27
  belongs_to :stock, class_name: 'Iro::Stock', inverse_of: :positions
24
- def ticker
25
- stock&.ticker || '-'
26
- end
28
+ delegate :ticker, to: :stock
27
29
 
28
30
  belongs_to :strategy, class_name: 'Iro::Strategy', inverse_of: :positions
31
+ field :long_or_short
32
+
33
+ def put_call
34
+ case strategy.kind
35
+ when Iro::Strategy::KIND_LONG_DEBIT_CALL_SPREAD
36
+ put_call = 'CALL'
37
+ when Iro::Strategy::KIND_SHORT_DEBIT_PUT_SPREAD
38
+ put_call = 'PUT'
39
+ when Iro::Strategy::KIND_COVERED_CALL
40
+ put_call = 'CALL'
41
+ end
42
+ end
29
43
 
30
- # field :ticker
31
- # validates :ticker, presence: true
44
+ belongs_to :prev, class_name: 'Iro::Position', inverse_of: :nxt, optional: true
45
+ belongs_to :autoprev, class_name: 'Iro::Position', inverse_of: :autonxt, optional: true
46
+ ## there are many of these, for viewing on the 'roll' view
47
+ has_many :nxt, class_name: 'Iro::Position', inverse_of: :prev
48
+ has_one :autonxt, class_name: 'Iro::Position', inverse_of: :autoprev
49
+
50
+ ## Options
32
51
 
33
- belongs_to :outer, class_name: 'Iro::Option', inverse_of: :outer
34
52
  belongs_to :inner, class_name: 'Iro::Option', inverse_of: :inner
53
+ belongs_to :outer, class_name: 'Iro::Option', inverse_of: :outer
54
+ accepts_nested_attributes_for :inner, :outer
35
55
 
36
56
  field :outer_strike, type: :float
37
57
  # validates :outer_strike, presence: true
38
58
 
39
59
  field :inner_strike, type: :float
40
- validates :inner_strike, presence: true
60
+ # validates :inner_strike, presence: true
41
61
 
42
62
  field :expires_on
43
63
  validates :expires_on, presence: true
@@ -47,18 +67,8 @@ class Iro::Position
47
67
  def q; quantity; end
48
68
 
49
69
  field :begin_on
50
- field :begin_outer_price, type: :float
51
- field :begin_outer_delta, type: :float
52
-
53
- field :begin_inner_price, type: :float
54
- field :begin_inner_delta, type: :float
55
70
 
56
71
  field :end_on
57
- field :end_outer_price, type: :float
58
- field :end_outer_delta, type: :float
59
-
60
- field :end_inner_price, type: :float
61
- field :end_inner_delta, type: :float
62
72
 
63
73
  def begin_delta
64
74
  strategy.send("begin_delta_#{strategy.kind}", self)
@@ -86,7 +96,7 @@ class Iro::Position
86
96
  end_delta: out[:delta],
87
97
  end_price: out[:last],
88
98
  })
89
- print '_'
99
+ print '^'
90
100
  end
91
101
 
92
102
  def net_percent
@@ -101,237 +111,172 @@ class Iro::Position
101
111
  def max_loss # each
102
112
  strategy.send("max_loss_#{strategy.kind}", self)
103
113
  end
104
- # def gain_loss_amount
105
- # strategy.send("gain_loss_amount_#{strategy.kind}", self)
106
- # end
107
-
108
-
109
- field :next_delta, type: :float
110
- field :next_outcome, type: :float
111
- field :next_symbol
112
- field :next_mark
113
- field :next_reasons, type: :array, default: []
114
- field :rollp, type: :float
115
114
 
116
115
 
117
- ## covered call
118
- # def sync
119
- # puts! [ inner_strike, expires_on, stock.ticker ], 'init sync'
120
- # out = Tda::Option.get_quote({
121
- # contractType: 'CALL',
122
- # strike: inner_strike,
123
- # expirationDate: expires_on,
124
- # ticker: stock.ticker,
125
- # })
126
- # puts! out, 'sync'
127
- # self.end_inner_price = ( out.bid + out.ask ) / 2
128
- # self.end_inner_delta = out.delta
129
- # end
130
-
131
- ## long call spread
132
- # def sync
133
- # # puts! [
134
- # # [ inner_strike, expires_on, stock.ticker ],
135
- # # [ outer_strike, expires_on, stock.ticker ],
136
- # # ], 'init sync inner, outer'
137
- # inner = Tda::Option.get_quote({
138
- # contractType: 'CALL',
139
- # strike: inner_strike,
140
- # expirationDate: expires_on,
141
- # ticker: stock.ticker,
142
- # })
143
- # outer = Tda::Option.get_quote({
144
- # contractType: 'CALL',
145
- # strike: outer_strike,
146
- # expirationDate: expires_on,
147
- # ticker: stock.ticker,
148
- # })
149
- # puts! [inner, outer], 'sync inner, outer'
150
- # self.end_outer_price = ( outer.bid + outer.ask ) / 2
151
- # self.end_outer_delta = outer.delta
152
-
153
- # self.end_inner_price = ( inner.bid + inner.ask ) / 2
154
- # self.end_inner_delta = inner.delta
155
- # end
156
-
157
116
  def sync
158
- put_call = Iro::Strategy::LONG == strategy.long_or_short ? 'CALL' : 'PUT'
159
- puts! [
160
- [ inner_strike, expires_on, stock.ticker ],
161
- [ outer_strike, expires_on, stock.ticker ],
162
- ], 'init sync inner, outer'
163
- inner = Tda::Option.get_quote({
164
- contractType: put_call,
165
- strike: inner_strike,
166
- expirationDate: expires_on,
167
- ticker: stock.ticker,
168
- })
169
- outer = Tda::Option.get_quote({
170
- contractType: put_call,
171
- strike: outer_strike,
172
- expirationDate: expires_on,
173
- ticker: stock.ticker,
174
- })
175
- puts! [inner, outer], 'sync inner, outer'
176
- self.end_outer_price = ( outer.bid + outer.ask ) / 2
177
- self.end_outer_delta = outer.delta
178
-
179
- self.end_inner_price = ( inner.bid + inner.ask ) / 2
180
- self.end_inner_delta = inner.delta
117
+ inner.sync
118
+ outer.sync
181
119
  end
182
- def sync_short_debit_put_spread
183
- puts! [
184
- [ inner_strike, expires_on, stock.ticker ],
185
- [ outer_strike, expires_on, stock.ticker ],
186
- ], 'init sync inner, outer'
187
- inner = Tda::Option.get_quote({
188
- contractType: 'PUT',
189
- strike: inner_strike,
190
- expirationDate: expires_on,
191
- ticker: stock.ticker,
192
- })
193
- outer = Tda::Option.get_quote({
194
- contractType: 'PUT',
195
- strike: outer_strike,
196
- expirationDate: expires_on,
197
- ticker: stock.ticker,
198
- })
199
- puts! [inner, outer], 'sync inner, outer'
200
- self.end_outer_price = ( outer.bid + outer.ask ) / 2
201
- self.end_outer_delta = outer.delta
202
120
 
203
- self.end_inner_price = ( inner.bid + inner.ask ) / 2
204
- self.end_inner_delta = inner.delta
205
- end
206
121
 
207
122
  ##
208
123
  ## decisions
209
124
  ##
210
125
 
126
+ field :next_reasons, type: :array, default: []
127
+ field :rollp, type: :float
128
+
129
+ ## should_roll?
211
130
  def calc_rollp
212
131
  self.next_reasons = []
213
- self.next_symbol = nil
214
- self.next_delta = nil
132
+ # self.next_symbol = nil
133
+ # self.next_delta = nil
215
134
 
216
135
  out = strategy.send( "calc_rollp_#{strategy.kind}", self )
217
136
 
218
137
  self.rollp = out[0]
219
138
  self.next_reasons.push out[1]
220
139
  save
221
-
222
- # update({
223
- # next_delta: next_position[:delta],
224
- # next_outcome: next_position[:mark] - end_price,
225
- # next_symbol: next_position[:symbol],
226
- # next_mark: next_position[:mark],
227
- # should_rollp: out,
228
- # # status: Iro::Position::STATE_PROPOSED,
229
- # })
230
- end
231
-
232
-
233
- ## expires_on = cc.expires_on ; nil
234
- def can_roll?
235
- ## only if less than 7 days left
236
- ( expires_on.to_date - Time.now.to_date ).to_i < 7
237
140
  end
238
141
 
239
- ## strike = cc.strike ; strategy = cc.strategy ; nil
240
- def near_below_water?
241
- strike < current_underlying_strike + strategy.buffer_above_water
242
- end
243
-
244
-
245
-
246
- ## 2023-03-18 _vp_ Continue.
247
- ## 2023-03-19 _vp_ Continue.
248
- ## 2023-08-05 _vp_ an Important method
249
- ##
250
- ## expires_on = cc.expires_on ; strategy = cc.strategy ; ticker = cc.ticker ; end_price = cc.end_price ; next_expires_on = cc.next_expires_on ; nil
251
- ##
252
- ## out.map { |p| [ p[:strikePrice], p[:delta] ] }
253
- ##
254
- def next_position
255
- return @next_position if @next_position
256
- return {} if ![ STATUS_ACTIVE, STATUS_PROPOSED ].include?( status )
142
+ def calc_nxt
143
+ pos = self
257
144
 
258
145
  ## 7 days ahead - not configurable so far
259
- out = Tda::Option.get_quotes({
260
- ticker: ticker,
146
+ outs = Tda::Option.get_quotes({
147
+ contractType: pos.put_call,
261
148
  expirationDate: next_expires_on,
262
- contractType: 'CALL',
149
+ ticker: ticker,
263
150
  })
151
+ outs_bk = outs.dup
152
+
153
+ # byebug
264
154
 
265
- ## above_water
266
- if strategy.buffer_above_water.present?
267
- out = out.select do |i|
268
- i[:strikePrice] > current_underlying_strike + strategy.buffer_above_water
155
+ ## strike price
156
+ outs = outs.select do |out|
157
+ out[:bidSize]+out[:askSize] > 0
158
+ end
159
+ outs = outs.select do |out|
160
+ if Iro::Strategy::SHORT == pos.long_or_short
161
+ out[:strikePrice] > strategy.next_buffer_above_water + strategy.stock.last
162
+ elsif Iro::Strategy::LONG == pos.long_or_short
163
+ out[:strikePrice] < strategy.stock.last - strategy.next_buffer_above_water
164
+ else
165
+ throw 'zz4 - this cannot happen'
166
+ end
167
+ end
168
+ puts! outs[0][:strikePrice], 'after calc next_buffer_above_water'
169
+
170
+ outs = outs.select do |out|
171
+ if Iro::Strategy::SHORT == pos.long_or_short
172
+ out[:strikePrice] > strategy.next_inner_strike
173
+ elsif Iro::Strategy::LONG == pos.long_or_short
174
+ out[:strikePrice] < strategy.next_inner_strike
175
+ else
176
+ throw 'zz3 - this cannot happen'
269
177
  end
270
- # next_reasons.push "buffer_above_water above #{current_underlying_strike + strategy.buffer_above_water}"
271
178
  end
179
+ puts! outs[0][:strikePrice], 'after calc next_inner_strike'
180
+
181
+ ## delta
182
+ outs = outs.select do |out|
183
+ out_delta = out[:delta].abs rescue 0
184
+ out_delta >= strategy.next_inner_delta.abs
185
+ end
186
+ puts! outs[0][:strikePrice], 'after calc next_inner_delta'
272
187
 
273
- if near_below_water?
274
- msg = "Panic! climb at a loss. Skip the rest of the calculation."
275
- next_reasons.push msg
276
- ## @TODO: if not enough money in the purse, cannot roll? 2023-03-19
188
+ inner = outs[0]
189
+ outs = outs.select do |out|
190
+ out[:strikePrice] >= inner[:strikePrice].to_f + strategy.next_spread_amount
191
+ end
192
+ outer = outs[0]
193
+
194
+ # byebug
195
+
196
+ if inner && outer
197
+ o_attrs = {
198
+ expires_on: next_expires_on,
199
+ put_call: pos.put_call,
200
+ stock_id: pos.stock_id,
201
+ }
202
+ inner_ = Iro::Option.new(o_attrs.merge({
203
+ strike: inner[:strikePrice],
204
+ begin_price: ( inner[:bid] + inner[:ask] )/2,
205
+ begin_delta: inner[:delta],
206
+ end_price: ( inner[:bid] + inner[:ask] )/2,
207
+ end_delta: inner[:delta],
208
+ }))
209
+ outer_ = Iro::Option.new(o_attrs.merge({
210
+ strike: outer[:strikePrice],
211
+ begin_price: ( outer[:bid] + outer[:ask] )/2,
212
+ begin_delta: outer[:delta],
213
+ end_price: ( outer[:bid] + outer[:ask] )/2,
214
+ end_delta: outer[:delta],
215
+ }))
216
+ pos.autonxt ||= Iro::Position.new
217
+ pos.autonxt.update({
218
+ prev_gain_loss_amount: 'a',
219
+ status: 'proposed',
220
+ stock: strategy.stock,
221
+ inner: inner_,
222
+ outer: outer_,
223
+ inner_strike: inner_.strike,
224
+ outer_strike: outer_.strike,
225
+ begin_on: Time.now.to_date,
226
+ expires_on: next_expires_on,
227
+ purse: purse,
228
+ strategy: strategy,
229
+ quantity: 1,
230
+ autoprev: pos,
231
+ })
277
232
 
278
233
  # byebug
279
234
 
280
- ## Take a small loss here.
281
- prev = nil
282
- out.each_with_index do |i, idx|
283
- next if idx == 0
284
- if i[:last] < end_price
285
- prev ||= i
286
- end
287
- end
288
- out = [ prev ]
235
+ autonxt.sync
236
+ autonxt.save!
289
237
 
290
238
  else
291
- ## Normal flow, making money.
292
- ## @TODO: test! _vp_ 2023-03-19
293
-
294
- ## next_min_strike
295
- if strategy.next_min_strike.present?
296
- out = out.select do |i|
297
- i[:strikePrice] >= strategy.next_min_strike
298
- end
299
- # next_reasons.push "next_min_strike above #{strategy.next_min_strike}"
300
- end
301
- # json_puts! out.map { |p| [p[:delta], p[:symbol]] }, 'next_min_strike'
302
-
303
- ## max_delta
304
- if strategy.next_max_delta.present?
305
- out = out.select do |i|
306
- i[:delta] = 0.0 if i[:delta] == "NaN"
307
- i[:delta] <= strategy.next_max_delta
308
- end
309
- # next_reasons.push "next_max_delta below #{strategy.next_max_delta}"
310
- end
311
- # json_puts! out.map { |p| [p[:delta], p[:symbol]] }, 'next_max_delta'
239
+ throw 'zmq - should not happen'
312
240
  end
313
-
314
- @next_position = out[0] || {}
315
241
  end
316
242
 
317
- ## @TODO: Test this. _vp_ 2023-04-01
243
+
244
+
245
+ ## ok
318
246
  def next_expires_on
319
- out = expires_on.to_time + 7.days
320
- while !out.friday?
321
- out = out + 1.day
322
- end
323
- while !out.workday?
324
- out = out - 1.day
247
+ out = expires_on.to_datetime.next_occurring(:monday).next_occurring(:friday)
248
+ if !out.workday?
249
+ out = Time.previous_business_day(out )
325
250
  end
326
251
  return out
327
252
  end
328
253
 
254
+ ## ok
255
+ def self.long
256
+ where( long_or_short: Iro::Strategy::LONG )
257
+ end
258
+
259
+ ## ok
260
+ def self.short
261
+ where( long_or_short: Iro::Strategy::SHORT )
262
+ end
263
+
329
264
  def to_s
330
265
  out = "#{stock} (#{q}) #{expires_on.to_datetime.strftime('%b %d')} #{strategy.kind_short} ["
331
- if outer_strike
332
- out = out + "$#{outer_strike}->"
266
+ if Iro::Strategy::LONG == long_or_short
267
+ if outer.strike
268
+ out = out + "$#{outer.strike}->"
269
+ end
270
+ out = out + "$#{inner.strike}"
271
+ else
272
+ out = out + "$#{inner.strike}"
273
+ if outer.strike
274
+ out = out + "<-$#{outer.strike}"
275
+ end
333
276
  end
334
- out = out + "$#{inner_strike}] "
277
+ out += "] "
335
278
  return out
336
279
  end
337
280
  end
281
+
282
+
@@ -1,4 +1,7 @@
1
1
 
2
+ require 'distribution'
3
+ N = Distribution::Normal
4
+
2
5
  class Iro::Purse
3
6
  include Mongoid::Document
4
7
  include Mongoid::Timestamps
@@ -9,23 +12,50 @@ class Iro::Purse
9
12
  validates :slug, presence: true, uniqueness: true
10
13
  index({ slug: -1 }, { unique: true })
11
14
 
12
- has_many :positions, class_name: 'Iro::Position', inverse_of: :purse
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
13
18
 
14
19
  belongs_to :stock, class_name: 'Iro::Stock', inverse_of: :strategies
15
20
 
16
21
  field :unit, type: :integer, default: 10
22
+ ## with unit 10, .001
23
+ ## with unit 100, .0001
24
+ field :summary_unit, type: :float, default: 0.001
17
25
 
18
26
  ## for rolling only:
19
27
  field :height, type: :integer, default: 100
20
28
 
21
29
  field :mark_every_n_usd, type: :float, default: 1
22
30
  field :n_next_positions, type: :integer, default: 5
23
- ## with unit 10, sum_scale .001
24
- ## with unit 100, sum_scale .0001
25
- field :summary_scale, type: :float, default: 0.001
26
31
 
27
32
  field :available_amount, type: :float
28
33
 
34
+ def delta_wt_avg( begin_end, long_short, inner_outer )
35
+ max_loss_total = 0
36
+
37
+ out = positions.send( long_short ).map do |pos|
38
+ max_loss_total += pos.max_loss * pos.q
39
+ pos.max_loss * pos.q * pos.send( inner_outer ).send( "#{begin_end}_delta" )
40
+ end
41
+ puts! out, 'delta_wt_avg 1'
42
+ out = out.reduce( &:+ ) / max_loss_total rescue 0
43
+ puts! out, 'delta_wt_avg 2'
44
+ return out
45
+ end
46
+ ## delta to plot percentage
47
+ ## convert to normal between 0 and 3 std
48
+ def delta_to_plot_p( *args )
49
+ x = delta_wt_avg( *args ).abs
50
+ if x < 0.5
51
+ y = 1
52
+ else
53
+ y = 2 - 1/( 1.5 - x )
54
+ end
55
+ y_ = "#{ (y*100) .to_i}%"
56
+ return y_
57
+ end
58
+
29
59
  def to_s
30
60
  slug
31
61
  end