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