iron_warbler 2.0.7.7 → 2.0.7.9

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