iron_warbler 2.0.7.7 → 2.0.7.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +3 -16
  3. data/app/assets/javascript/iron_warbler/application.js +3 -0
  4. data/app/assets/stylesheets/iron_warbler/positions.scss +47 -0
  5. data/app/assets/stylesheets/iron_warbler/strategies.scss +0 -0
  6. data/app/assets/stylesheets/iron_warbler/utils.css +44 -0
  7. data/app/controllers/iro/alerts_controller.rb +6 -6
  8. data/app/controllers/iro/application_controller.rb +3 -2
  9. data/app/controllers/iro/datapoints_controller.rb +0 -2
  10. data/app/controllers/iro/positions_controller.rb +62 -0
  11. data/app/controllers/iro/profiles_controller.rb +0 -2
  12. data/app/controllers/iro/purses_controller.rb +65 -0
  13. data/app/controllers/iro/stocks_controller.rb +11 -4
  14. data/app/controllers/iro/strategies_controller.rb +66 -0
  15. data/app/models/iro/alert.rb +15 -4
  16. data/app/models/iro/datapoint.rb +108 -3
  17. data/app/models/iro/date.rb +10 -0
  18. data/app/models/iro/option.rb +13 -5
  19. data/app/models/iro/position.rb +222 -0
  20. data/app/models/iro/price_item.rb +5 -2
  21. data/app/models/iro/purse.rb +17 -0
  22. data/app/models/iro/stock.rb +19 -8
  23. data/app/models/iro/strategy.rb +38 -0
  24. data/app/models/tda/option.rb +137 -0
  25. data/app/views/iro/_analytics.erb +16 -0
  26. data/app/views/iro/_main_header.haml +23 -0
  27. data/app/views/iro/alerts/index.haml +1 -1
  28. data/app/views/iro/positions/_form.haml +59 -0
  29. data/app/views/iro/positions/_reasons.haml +4 -0
  30. data/app/views/iro/positions/_table.haml +137 -0
  31. data/app/views/iro/positions/edit.haml +4 -0
  32. data/app/views/iro/positions/new.haml +4 -0
  33. data/app/views/iro/purses/_form.haml +9 -0
  34. data/app/views/iro/purses/edit.haml +3 -0
  35. data/app/views/iro/purses/index.haml +11 -0
  36. data/app/views/iro/purses/show.haml +10 -0
  37. data/app/views/iro/stocks/_form.haml +4 -0
  38. data/app/views/iro/strategies/_form.haml +37 -0
  39. data/app/views/iro/strategies/_header.haml +8 -0
  40. data/app/views/iro/strategies/_show.haml +18 -0
  41. data/app/views/iro/strategies/_table.haml +18 -0
  42. data/app/views/iro/strategies/edit.haml +3 -0
  43. data/app/views/iro/strategies/new.haml +3 -0
  44. data/app/views/layouts/iro/application.haml +48 -13
  45. data/config/initializers/assets.rb +10 -0
  46. data/config/routes.rb +6 -0
  47. data/lib/iron_warbler.rb +4 -1
  48. data/lib/tasks/db_tasks.rake +54 -1
  49. data/lib/tasks/iro_tasks.rake +35 -0
  50. data/lib/tasks/iro_tasks.rake-old +128 -0
  51. metadata +122 -40
  52. data/app/assets/stylesheets/iron_warbler/main.css +0 -13
  53. data/app/assets/stylesheets/scaffold.css +0 -80
  54. data/app/models/iro/application_record.rb +0 -4
  55. data/app/models/iro/profile.rb +0 -4
  56. data/app/views/iro/alerts/edit.html.erb +0 -6
  57. data/app/views/iro/alerts/new.html.erb +0 -5
  58. data/app/views/iro/alerts/show.html.erb +0 -34
  59. data/app/views/iro/profiles/_form.html.erb +0 -32
  60. data/app/views/iro/profiles/edit.html.erb +0 -6
  61. data/app/views/iro/profiles/index.html.erb +0 -31
  62. data/app/views/iro/profiles/new.html.erb +0 -5
  63. data/app/views/iro/profiles/show.html.erb +0 -19
  64. data/db/migrate/20231210204830_create_iro_profiles.rb +0 -12
  65. data/db/migrate/20231210205837_create_iro_alerts.rb +0 -16
  66. data/db/migrate/20231219204329_create_dates.rb +0 -13
  67. data/db/migrate/20231219205644_create_datapoint.rb +0 -15
  68. data/db/migrate/20231220193201_create_stocks.rb +0 -11
  69. data/db/migrate/20231220194903_add_alert_status.rb +0 -5
  70. data/db/migrate/20231220223730_create_iro_price_item.rb +0 -57
  71. /data/app/models/tda/{api.rb → stock.rb} +0 -0
@@ -0,0 +1,222 @@
1
+
2
+ class Iro::Position
3
+ include Mongoid::Document
4
+ include Mongoid::Timestamps
5
+ store_in collection: 'iro_positions'
6
+
7
+ STATUS_ACTIVE = 'active'
8
+ STATUS_PROPOSED = 'proposed'
9
+ STATUSES = [ nil, 'active', 'inactive', 'proposed' ]
10
+ field :status
11
+ validates :status, presence: true
12
+ scope :active, ->{ where( status: 'active' ) }
13
+
14
+ belongs_to :purse, class_name: 'Iro::Purse', inverse_of: :positions
15
+ belongs_to :strategy, class_name: 'Iro::Strategy', inverse_of: :positions
16
+
17
+ field :ticker
18
+ validates :ticker, presence: true
19
+ index({ purse_id: 1, ticker: 1 })
20
+
21
+ KINDS = [ nil, 'covered_call', 'credit_put_spread', 'credit_call_spread' ]
22
+ field :kind
23
+
24
+ field :strike, type: :float
25
+ validates :strike, presence: true
26
+
27
+ field :expires_on
28
+ validates :expires_on, presence: true
29
+
30
+ field :quantity, type: :integer
31
+ validates :quantity, presence: true
32
+
33
+ field :begin_on
34
+ field :begin_price, type: :float
35
+ field :begin_delta, type: :float
36
+
37
+ field :end_on
38
+ field :end_price, type: :float
39
+ field :end_delta, type: :float
40
+
41
+ field :net_amount
42
+ field :net_percent
43
+
44
+ def current_underlying_strike
45
+ Iro::Stock.find_by( ticker: ticker ).last
46
+ end
47
+
48
+ def refresh
49
+ out = Tda::Option.get_quote({
50
+ contractType: 'CALL',
51
+ strike: strike,
52
+ expirationDate: expires_on,
53
+ ticker: ticker,
54
+ })
55
+ update({
56
+ end_delta: out[:delta],
57
+ end_price: out[:last],
58
+ })
59
+ print '_'
60
+ end
61
+
62
+
63
+ field :next_delta, type: :float
64
+ field :next_outcome, type: :float
65
+ field :next_symbol
66
+ field :next_mark
67
+ field :next_reasons, type: :array, default: []
68
+ field :should_rollp, type: :float
69
+
70
+ def should_roll?
71
+ puts! 'shold_roll?'
72
+
73
+ update({
74
+ next_reasons: [],
75
+ next_symbol: nil,
76
+ next_delta: nil,
77
+ })
78
+
79
+ if must_roll?
80
+ out = 1.0
81
+ elsif can_roll?
82
+
83
+ if end_delta < strategy.threshold_delta
84
+ next_reasons.push "delta is lower than threshold"
85
+ out = 0.91
86
+ elsif 1 - end_price/begin_price > strategy.threshold_netp
87
+ next_reasons.push "made enough percent profit (dubious)"
88
+ out = 0.61
89
+ else
90
+ next_reasons.push "neutral"
91
+ out = 0.33
92
+ end
93
+
94
+ else
95
+ out = 0.0
96
+ end
97
+
98
+ update({
99
+ next_delta: next_position[:delta],
100
+ next_outcome: next_position[:mark] - end_price,
101
+ next_symbol: next_position[:symbol],
102
+ next_mark: next_position[:mark],
103
+ should_rollp: out,
104
+ # status: Iro::Position::STATE_PROPOSED,
105
+ })
106
+
107
+ puts! next_reasons, 'next_reasons'
108
+ puts! out, 'out'
109
+ return out > 0.5
110
+ end
111
+
112
+
113
+ ## expires_on = cc.expires_on ; nil
114
+ def can_roll?
115
+ ## only if less than 7 days left
116
+ ( expires_on.to_date - Time.now.to_date ).to_i < 7
117
+ end
118
+
119
+ ## If I'm near below water
120
+ ##
121
+ ## expires_on = cc.expires_on ; strategy = cc.strategy ; strike = cc.strike ; nil
122
+ def must_roll?
123
+ if ( current_underlying_strike + strategy.buffer_above_water ) > strike
124
+ return true
125
+ end
126
+ ## @TODO: This one should not happen, I should log appropriately. _vp_ 2023-03-19
127
+ if ( expires_on.to_date - Time.now.to_date ).to_i < 1
128
+ return true
129
+ end
130
+ end
131
+
132
+ ## strike = cc.strike ; strategy = cc.strategy ; nil
133
+ def near_below_water?
134
+ strike < current_underlying_strike + strategy.buffer_above_water
135
+ end
136
+
137
+
138
+
139
+ ## 2023-03-18 _vp_ Continue.
140
+ ## 2023-03-19 _vp_ Continue.
141
+ ## 2023-08-05 _vp_ an Important method
142
+ ##
143
+ ## expires_on = cc.expires_on ; strategy = cc.strategy ; ticker = cc.ticker ; end_price = cc.end_price ; next_expires_on = cc.next_expires_on ; nil
144
+ ##
145
+ ## out.map { |p| [ p[:strikePrice], p[:delta] ] }
146
+ ##
147
+ def next_position
148
+ return @next_position if @next_position
149
+ return {} if ![ STATUS_ACTIVE, STATUS_PROPOSED ].include?( status )
150
+
151
+ ## 7 days ahead - not configurable so far
152
+ out = Tda::Option.get_quotes({
153
+ ticker: ticker,
154
+ expirationDate: next_expires_on,
155
+ contractType: 'CALL',
156
+ })
157
+
158
+ ## above_water
159
+ if strategy.buffer_above_water.present?
160
+ out = out.select do |i|
161
+ i[:strikePrice] > current_underlying_strike + strategy.buffer_above_water
162
+ end
163
+ # next_reasons.push "buffer_above_water above #{current_underlying_strike + strategy.buffer_above_water}"
164
+ end
165
+
166
+ if near_below_water?
167
+ msg = "Panic! climb at a loss. Skip the rest of the calculation."
168
+ next_reasons.push msg
169
+ ## @TODO: if not enough money in the purse, cannot roll? 2023-03-19
170
+
171
+ # byebug
172
+
173
+ ## Take a small loss here.
174
+ prev = nil
175
+ out.each_with_index do |i, idx|
176
+ next if idx == 0
177
+ if i[:last] < end_price
178
+ prev ||= i
179
+ end
180
+ end
181
+ out = [ prev ]
182
+
183
+ else
184
+ ## Normal flow, making money.
185
+ ## @TODO: test! _vp_ 2023-03-19
186
+
187
+ ## next_min_strike
188
+ if strategy.next_min_strike.present?
189
+ out = out.select do |i|
190
+ i[:strikePrice] >= strategy.next_min_strike
191
+ end
192
+ # next_reasons.push "next_min_strike above #{strategy.next_min_strike}"
193
+ end
194
+ # json_puts! out.map { |p| [p[:delta], p[:symbol]] }, 'next_min_strike'
195
+
196
+ ## max_delta
197
+ if strategy.next_max_delta.present?
198
+ out = out.select do |i|
199
+ i[:delta] = 0.0 if i[:delta] == "NaN"
200
+ i[:delta] <= strategy.next_max_delta
201
+ end
202
+ # next_reasons.push "next_max_delta below #{strategy.next_max_delta}"
203
+ end
204
+ # json_puts! out.map { |p| [p[:delta], p[:symbol]] }, 'next_max_delta'
205
+ end
206
+
207
+ @next_position = out[0] || {}
208
+ end
209
+
210
+ ## @TODO: Test this. _vp_ 2023-04-01
211
+ def next_expires_on
212
+ out = expires_on.to_time + 7.days
213
+ while !out.friday?
214
+ out = out + 1.day
215
+ end
216
+ while !out.workday?
217
+ out = out - 1.day
218
+ end
219
+ return out
220
+ end
221
+
222
+ end
@@ -1,4 +1,7 @@
1
1
 
2
- class Iro::PriceItem < Iro::ApplicationRecord
3
- self.table_name = 'iro_price_items'
2
+ class Iro::PriceItem
3
+ include Mongoid::Document
4
+ include Mongoid::Timestamps
5
+ store_in collection: 'iro_price_items'
6
+
4
7
  end
@@ -0,0 +1,17 @@
1
+
2
+ class Iro::Purse
3
+ include Mongoid::Document
4
+ include Mongoid::Timestamps
5
+ store_in collection: 'iro_purses'
6
+
7
+ field :slug
8
+ validates :slug, presence: true, uniqueness: true
9
+ index({ slug: -1 }, { unique: true })
10
+
11
+ has_many :positions, class_name: 'Iro::Position', inverse_of: :purse
12
+
13
+ def to_s
14
+ slug
15
+ end
16
+
17
+ end
@@ -1,18 +1,29 @@
1
1
 
2
- ##
3
- ## SQL
4
- ##
5
- class Iro::Stock < Iro::ApplicationRecord
6
- self.table_name = 'iro_stocks'
2
+ class Iro::Stock
3
+ include Mongoid::Document
4
+ include Mongoid::Timestamps
5
+ store_in collection: 'iro_stocks'
7
6
 
8
7
  STATUS_ACTIVE = 'active'
9
8
  STATUS_INACTIVE = 'inactive'
10
- STATUSES = [ 'active', 'inactive' ]
9
+ STATUSES = [ nil, 'active', 'inactive' ]
10
+ def self.active
11
+ where( status: STATUS_ACTIVE )
12
+ end
13
+ field :status
11
14
 
15
+ field :ticker
12
16
  validates :ticker, uniqueness: true, presence: true
13
17
 
14
- def self.active
15
- where( status: STATUS_ACTIVE )
18
+ field :last, type: :float
19
+
20
+ def self.list
21
+ end
22
+
23
+ def self.tickers_list
24
+ [nil] + all.map( &:ticker )
16
25
  end
17
26
 
27
+ # has_many :strategies, class_name: 'Iro::Strategy', inverse_of: :stock
28
+
18
29
  end
@@ -0,0 +1,38 @@
1
+
2
+ class Iro::Strategy
3
+ include Mongoid::Document
4
+ include Mongoid::Timestamps
5
+ store_in collection: 'iro_strategies'
6
+
7
+ field :slug
8
+ validates :slug, presence: true, uniqueness: true
9
+
10
+ has_many :positions, class_name: 'Iro::Position', inverse_of: :strategy
11
+
12
+ ## multiple strategies per ticker
13
+ field :ticker
14
+ validates :ticker, presence: true
15
+ index({ ticker: 1 })
16
+ # belongs_to :stock, class_name: 'Iro::Stock', inverse_of: :strategies
17
+
18
+
19
+ field :buffer_above_water, type: :float
20
+ field :next_max_delta, type: :float
21
+ field :next_min_strike, type: :float
22
+ field :threshold_delta, type: :float
23
+ field :threshold_netp, type: :float
24
+
25
+ def self.for_ticker ticker
26
+ where( ticker: ticker )
27
+ end
28
+
29
+
30
+ def to_s
31
+ slug
32
+ end
33
+
34
+ def self.list
35
+ [[nil,nil]] + all.map { |ttt| [ ttt.slug, ttt.id ] }
36
+ end
37
+
38
+ end
@@ -0,0 +1,137 @@
1
+
2
+ require 'httparty'
3
+
4
+ class Tda::Option
5
+
6
+ include ::HTTParty
7
+ base_uri 'https://api.tdameritrade.com'
8
+
9
+
10
+ ##
11
+ ## 2023-02-05 _vp_ :: Gets the entire chain
12
+ ##
13
+ def self.get_chain params
14
+ opts = { symbol: params[:ticker] } ## use 'GME' as symbol here even though a symbol is eg 'GME_021023P2.5'
15
+ query = { apikey: ::TD_AMERITRADE[:apiKey] }.merge opts
16
+ # puts! query, 'input opts'
17
+
18
+ path = "/v1/marketdata/chains"
19
+ out = self.get path, { query: query }
20
+ timestamp = DateTime.parse out.headers['date']
21
+ out = out.parsed_response.deep_symbolize_keys
22
+
23
+
24
+ outs = []
25
+ %w| put call |.each do |contractType|
26
+ tmp_sym = "#{contractType}ExpDateMap".to_sym
27
+ _out = out[tmp_sym]
28
+ _out.each do |date, vs| ## date="2023-02-10:5"
29
+ vs.each do |strike, _v| ## strike="18.5"
30
+ v = _v[0] ## v={} many attrs
31
+ v = v.except( :lastSize, :optionDeliverablesList, :settlementType,
32
+ :deliverableNote, :pennyPilot, :mini )
33
+ v.each do |k, i|
34
+ if i == 'NaN'
35
+ v[k] = nil
36
+ end
37
+ end
38
+
39
+ v[:timestamp] = timestamp
40
+ v[:ticker] = params[:ticker]
41
+ outs.push( v )
42
+ end
43
+ end
44
+ end
45
+
46
+ outs.each do |x|
47
+ opi = ::Iro::OptionPriceItem.create( x )
48
+ if !opi.persisted?
49
+ puts! opi.errors.full_messages, "Cannot create OptionPriceItem"
50
+ end
51
+ end
52
+ end
53
+
54
+ ##
55
+ ## 2023-03-18 _vp_ This is what I should be using to check if a position should be rolled.
56
+ ##
57
+ def self.get_quote params
58
+ ::Tda::Option.get_quotes(params)[0]
59
+ end
60
+
61
+ ##
62
+ ## params: contractType, strike, expirationDate, ticker
63
+ ##
64
+ ## ow = { contractType: 'PUT', ticker: 'GME', date: '2022-12-09' }
65
+ ## query = {:apikey=>"<>", :toDate=>"2022-12-09", :fromDate=>"2022-12-09", :symbol=>"GME"}
66
+ ##
67
+ ## 2023-02-04 _vp_ :: Too specific, but I want the entire chain, every 1-min
68
+ ## 2023-02-06 _vp_ :: Continue.
69
+ ##
70
+ def self.get_quotes params
71
+ puts! params, 'Tda::Option#get_quotes'
72
+ opts = {}
73
+
74
+ #
75
+ # Validate input ???
76
+ #
77
+ validOpts = %i| contractType |
78
+ validOpts.each do |s|
79
+ if params[s]
80
+ opts[s] = params[s]
81
+ else
82
+ raise Iwa::InputError.new("Invalid input, missing '#{s}'.")
83
+ end
84
+ end
85
+ if params[:expirationDate]
86
+ opts[:fromDate] = opts[:toDate] = params[:expirationDate]
87
+ else
88
+ raise Iwa::InputError.new("Invalid input, missing 'date'.")
89
+ end
90
+ if params[:ticker]
91
+ opts[:symbol] = params[:ticker].upcase
92
+ else
93
+ raise Iwa::InputError.new("Invalid input, missing 'ticker'.")
94
+ end
95
+
96
+ if params[:strike]
97
+ opts[:strike] = params[:strike]
98
+ end
99
+
100
+ query = { apikey: ::TD_AMERITRADE[:apiKey] }.merge opts
101
+ # puts! query, 'input opts'
102
+
103
+ path = "/v1/marketdata/chains"
104
+ out = self.get path, { query: query }
105
+ timestamp = DateTime.parse out.headers['date']
106
+ ## out = HTTParty.get "https://api.tdameritrade.com#{path}", { query: query }
107
+ out = out.parsed_response.deep_symbolize_keys
108
+
109
+
110
+ tmp_sym = "#{opts[:contractType].to_s.downcase}ExpDateMap".to_sym
111
+ outs = []
112
+ out = out[tmp_sym]
113
+ out.each do |date, vs|
114
+ vs.each do |strike, _v|
115
+ v = _v[0]
116
+ v = v.except( :lastSize, :optionDeliverablesList, :settlementType,
117
+ :deliverableNote, :pennyPilot, :mini )
118
+ v[:timestamp] = timestamp
119
+ outs.push( v )
120
+ end
121
+ end
122
+
123
+ # puts! outs, 'outs'
124
+ return outs
125
+ end
126
+
127
+
128
+ end
129
+
130
+ =begin
131
+
132
+ outs = Tda::Option.get_quotes({
133
+ contractType: 'CALL', strike: 20.0, expirationDate: '2024-01-12',
134
+ ticker: 'GME',
135
+ })
136
+
137
+ =end
@@ -0,0 +1,16 @@
1
+
2
+ <!-- Matomo -->
3
+ <script>
4
+ var _paq = window._paq = window._paq || [];
5
+ /* tracker methods like "setCustomDimension" should be called before "trackPageView" */
6
+ _paq.push(['trackPageView']);
7
+ _paq.push(['enableLinkTracking']);
8
+ (function() {
9
+ var u="//analytics.wasya.co/";
10
+ _paq.push(['setTrackerUrl', u+'matomo.php']);
11
+ _paq.push(['setSiteId', '13']); /* 13 :: email.wasya.co */
12
+ var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
13
+ g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
14
+ })();
15
+ </script>
16
+ <!-- End Matomo Code -->
@@ -0,0 +1,23 @@
1
+
2
+ .main-header.maxwidth
3
+
4
+ %i.fa.fa-compress.collapse-expand#collapseHeaderEmail
5
+ Iron Warbler
6
+
7
+ -# .header.collapse-expand#IroMenu
8
+ -# %h5.title Iron Warbler
9
+
10
+ %ul
11
+ %li= link_to 'ROOT', root_path
12
+ %li
13
+ = link_to "Stocks (#{Iro::Stock.all.length})", stocks_path
14
+ -# %li= link_to 'Options', options_path
15
+ -# %li Max Pain
16
+ %li
17
+ = link_to "Alerts (#{Iro::Alert.all.length})", alerts_path
18
+ %li
19
+ = link_to "Purses (#{Iro::Purse.all.length})", purses_path
20
+ %li
21
+ = render '/iro/strategies/header'
22
+
23
+
@@ -1,5 +1,5 @@
1
1
 
2
- .iro-alerts--index
2
+ .iro-alerts--index.maxwidth
3
3
  %h5 Iro Alerts
4
4
 
5
5
  %ul
@@ -0,0 +1,59 @@
1
+
2
+ .positions--form
3
+ = form_for position do |f|
4
+ .actions
5
+ = f.submit
6
+
7
+ .field
8
+ %label Purse
9
+ -# = f.select :purse_id, options_for_select( @
10
+ = f.text_field :purse_id
11
+
12
+ %label Status
13
+ = f.select :status, options_for_select( Iro::Position::STATUSES, selected: position.status )
14
+
15
+ %br
16
+ %br
17
+
18
+ .field
19
+ %label Ticker
20
+ = f.select :ticker, options_for_select( @tickers_list, selected: position.ticker )
21
+
22
+
23
+ %label Kind
24
+ = f.select :kind, options_for_select( Iro::Position::KINDS, selected: position.kind )
25
+ %label Strategy
26
+ = f.select :strategy_id, options_for_select( @strategies_list, selected: position.strategy_id )
27
+
28
+
29
+ %label Strike
30
+ = f.text_field :strike
31
+
32
+ .field
33
+ %label Expires on
34
+ = f.text_field :expires_on
35
+
36
+
37
+ %label Quantity
38
+ = f.text_field :quantity
39
+
40
+ %br
41
+ %br
42
+
43
+ .flex-row
44
+ %label Begin on
45
+ = f.text_field :begin_on
46
+ %label Begin price
47
+ = f.text_field :begin_price
48
+ %label Begin delta
49
+ = f.text_field :begin_delta
50
+ .flex-row
51
+ %label End on
52
+ = f.text_field :end_on
53
+ %label End price
54
+ = f.text_field :end_price
55
+ %label End delta
56
+ = f.text_field :end_delta
57
+
58
+ .actions
59
+ = f.submit
@@ -0,0 +1,4 @@
1
+
2
+ %ul.positions--reasons.modal-absolute.hide
3
+ - reasons.each do |nnn|
4
+ %li= nnn