iron_warbler 2.0.7.8 → 2.0.7.10

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/iron_warbler/positions.scss +47 -0
  3. data/app/assets/stylesheets/iron_warbler/strategies.scss +0 -0
  4. data/app/assets/stylesheets/iron_warbler/utils.css +44 -0
  5. data/app/controllers/iro/alerts_controller.rb +6 -6
  6. data/app/controllers/iro/datapoints_controller.rb +0 -2
  7. data/app/controllers/iro/positions_controller.rb +62 -0
  8. data/app/controllers/iro/profiles_controller.rb +0 -2
  9. data/app/controllers/iro/purses_controller.rb +65 -0
  10. data/app/controllers/iro/stocks_controller.rb +10 -4
  11. data/app/controllers/iro/strategies_controller.rb +66 -0
  12. data/app/models/iro/option.rb +0 -7
  13. data/app/models/iro/position.rb +222 -0
  14. data/app/models/iro/purse.rb +17 -0
  15. data/app/models/iro/stock.rb +12 -2
  16. data/app/models/iro/strategy.rb +38 -0
  17. data/app/models/tda/option.rb +137 -0
  18. data/app/models/tda/{api.rb → stock.rb} +1 -1
  19. data/app/views/iro/_main_header.haml +16 -6
  20. data/app/views/iro/alerts/index.haml +1 -1
  21. data/app/views/iro/positions/_form.haml +59 -0
  22. data/app/views/iro/positions/_reasons.haml +4 -0
  23. data/app/views/iro/positions/_table.haml +137 -0
  24. data/app/views/iro/positions/edit.haml +4 -0
  25. data/app/views/iro/positions/new.haml +4 -0
  26. data/app/views/iro/purses/_form.haml +9 -0
  27. data/app/views/iro/purses/edit.haml +3 -0
  28. data/app/views/iro/purses/index.haml +11 -0
  29. data/app/views/iro/purses/show.haml +10 -0
  30. data/app/views/iro/stocks/_form.haml +4 -0
  31. data/app/views/iro/strategies/_form.haml +37 -0
  32. data/app/views/iro/strategies/_header.haml +8 -0
  33. data/app/views/iro/strategies/_show.haml +18 -0
  34. data/app/views/iro/strategies/_table.haml +18 -0
  35. data/app/views/iro/strategies/edit.haml +3 -0
  36. data/app/views/iro/strategies/new.haml +3 -0
  37. data/config/routes.rb +6 -0
  38. data/lib/iron_warbler.rb +4 -0
  39. data/lib/tasks/iro_tasks.rake +35 -0
  40. metadata +42 -13
  41. data/app/assets/stylesheets/iron_warbler/main.css +0 -13
  42. data/app/assets/stylesheets/scaffold.css +0 -80
  43. data/app/views/iro/alerts/edit.html.erb +0 -6
  44. data/app/views/iro/alerts/new.html.erb +0 -5
  45. data/app/views/iro/alerts/show.html.erb +0 -34
  46. data/app/views/iro/profiles/_form.html.erb +0 -32
  47. data/app/views/iro/profiles/edit.html.erb +0 -6
  48. data/app/views/iro/profiles/index.html.erb +0 -31
  49. data/app/views/iro/profiles/new.html.erb +0 -5
  50. data/app/views/iro/profiles/show.html.erb +0 -19
@@ -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
@@ -1,7 +1,7 @@
1
1
 
2
2
  require 'httparty'
3
3
 
4
- class Tda::Api
4
+ class Tda::Stock
5
5
  include ::HTTParty
6
6
  base_uri 'https://api.tdameritrade.com'
7
7
 
@@ -1,13 +1,23 @@
1
1
 
2
+ .main-header.maxwidth
2
3
 
4
+ %i.fa.fa-compress.collapse-expand#collapseHeaderEmail
5
+ Iron Warbler
6
+
7
+ -# .header.collapse-expand#IroMenu
8
+ -# %h5.title Iron Warbler
3
9
 
4
- .main-menu
5
- %h5 Main Menu
6
10
  %ul
7
- %li= link_to 'Home - Dashboard', root_path
8
- %li= link_to 'Stocks', stocks_path
11
+ %li= link_to 'ROOT', root_path
12
+ %li
13
+ = link_to "Stocks (#{Iro::Stock.all.length})", stocks_path
9
14
  -# %li= link_to 'Options', options_path
10
- %li Max Pain
11
- %li= link_to 'Alerts', alerts_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'
12
22
 
13
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
@@ -0,0 +1,137 @@
1
+
2
+ .positions--table
3
+
4
+ %h5 Positions
5
+ %table.bordered
6
+ %tr
7
+ %th &nbsp;
8
+ %th
9
+ .status
10
+ Status
11
+ %br
12
+ Strategy
13
+ %th.ticker
14
+ .a Ticker
15
+ %th
16
+ .kind
17
+ Kind
18
+ -# covered call (mounted elephant),
19
+ -# reckless defence (credit put spread) (spearman)
20
+ -# reckless re-defence (soaring eagle)
21
+ %th.strike
22
+ .a Strike
23
+ %th
24
+ .expires_on Expires On
25
+ %th
26
+ .end_n_days End N Days
27
+ %th
28
+ .quantity Q
29
+
30
+ %th
31
+ .begin_on begin_on
32
+ %th.begin_price
33
+ .a Begin price
34
+ %th.to_open
35
+ .a To open
36
+ %th
37
+ .begin_delta begin_delta
38
+
39
+ %th
40
+ .end_price End Price
41
+ %th
42
+ .net_amount Net Amount
43
+ %th
44
+ .net_percent Net %
45
+ %th.end_delta
46
+ .a end_delta
47
+ -# %th
48
+ -# .end_n_days End n days
49
+ -# %th
50
+ -# .end_on End on
51
+ %th.should_rollp
52
+ .a should_rollp
53
+ %th.next_position
54
+ .a
55
+ next_symbol
56
+ %br
57
+ next_delta
58
+ %br
59
+ next_outcome
60
+
61
+ - positions.each_with_index do |position, idx|
62
+ - if idx > 0
63
+ - prev_position = positions[idx-1]
64
+ - if prev_position && position[:expires_on] != prev_position[:expires_on]
65
+ %tr
66
+ %td{ colspan: 18 }
67
+ %hr
68
+
69
+ %tr
70
+ %td.actions
71
+ .flex-row
72
+ = button_to 'x', position_path(position), method: :delete, data: { confirm: 'Are you sure?' }
73
+ = link_to '[~]', edit_position_path(position)
74
+ - if position.next_reasons.present? && position.should_rollp && position.should_rollp > 0.5
75
+ = button_to 'Roll', roll_position_path(position)
76
+ - if position.next_reasons.present?
77
+ .collapse-expand{ id: "ce-p-#{position.id}" } [Reasons]
78
+ = render '/iro/positions/reasons', reasons: position.next_reasons
79
+
80
+
81
+ %td.status.strategy
82
+ = position.status
83
+ -# %br
84
+ -# = link_to "[~ #{position.strategy}]", edit_strategy_path(position.strategy)
85
+ .collapse-expand{ id: "ce-pos-reasons-#{position.id}" }
86
+ [#{position.strategy}]
87
+ = render '/iro/strategies/show', strategy: position.strategy
88
+
89
+ %td.ticker
90
+ = position.ticker
91
+ %br
92
+ = pp_amount position.current_underlying_strike
93
+ %td.kind.mini
94
+ = position.kind
95
+ %td.strike
96
+ = pp_amount position.strike
97
+ %td.expires_on.mini
98
+ = position.expires_on rescue nil
99
+ %td.end_n_days
100
+ -# = ( position.expires_on.to_date - position.begin_on.to_date ).to_i
101
+ %b= ( position.expires_on.to_date - Time.now.to_date ).to_i
102
+ %td= position.quantity
103
+
104
+ %td.begin_on.mini
105
+ = position.begin_on rescue nil
106
+ %td.begin_price
107
+ = pp_amount position.begin_price
108
+ %td.to_open
109
+ = pp_amount( position.begin_price * 100 * position.quantity )
110
+ %td= position.begin_delta
111
+
112
+ %td.end_price
113
+ = pp_amount position.end_price
114
+ %td.net_amount
115
+ = pp_amount ( position.begin_price - position.end_price ) * 100 * position.quantity rescue nil
116
+ %td.net_percent
117
+ = pp_percent( ( position.begin_price - position.end_price ) / position.begin_price ) rescue nil
118
+ %td.end_delta= position.end_delta
119
+ -# %td.end_n_days &nbsp;
120
+ -# %td.end_on &nbsp;
121
+
122
+ %td.should_rollp
123
+ = pp_percent position.should_rollp rescue nil
124
+ %td.next_position.mini
125
+ .a= position.next_symbol
126
+ .a= position.next_delta
127
+ .a
128
+ = pp_amount position.next_mark
129
+ \=>
130
+ = pp_amount position.next_outcome * 100 * position.quantity rescue nil
131
+
132
+
133
+
134
+
135
+
136
+
137
+
@@ -0,0 +1,4 @@
1
+
2
+ .positions-edit.maxwidth
3
+
4
+ = render 'form', position: @position
@@ -0,0 +1,4 @@
1
+
2
+ .positions-new.maxwidth
3
+ %h5 New Position
4
+ = render 'form', position: @position
@@ -0,0 +1,9 @@
1
+
2
+ .iro-purses--form
3
+ = form_for purse do |f|
4
+ .flex-row
5
+ .field
6
+ %label Slug
7
+ = f.text_field :slug
8
+ .actions
9
+ = f.submit
@@ -0,0 +1,3 @@
1
+
2
+ .purses-edit.maxwidth
3
+ = render 'form', purse: @purse
@@ -0,0 +1,11 @@
1
+
2
+ .iro-purses--index.maxwidth
3
+ %h5 Iro Purses
4
+
5
+ %ul
6
+ - @purses.each do |purse|
7
+ %li
8
+ = link_to '[~]', edit_purse_path(purse)
9
+ = link_to purse.slug, purse_path(purse)
10
+
11
+ %li= render 'iro/purses/form', purse: Iro::Purse.new
@@ -0,0 +1,10 @@
1
+
2
+ .purses-show.padded
3
+
4
+ .maxwidth
5
+ .header
6
+ %h5.title Purse `#{@purse.slug}` (#{@purse.positions.length})
7
+ %ul.menu-secondary
8
+ %li= link_to '[+position]', new_position_path({ purse_id: @purse.id })
9
+
10
+ = render '/iro/positions/table', positions: @purse.positions
@@ -11,5 +11,9 @@
11
11
  .field
12
12
  = f.label :ticker
13
13
  = f.text_field :ticker
14
+
15
+ .field
16
+ %label Last
17
+ = f.number_field :last, placeholder: '80.0', step: 0.01
14
18
  .actions
15
19
  = f.submit
@@ -0,0 +1,37 @@
1
+
2
+ .strategies--form
3
+ = form_for strategy do |f|
4
+ .actions
5
+ = f.submit
6
+
7
+ .field
8
+ %label Ticker
9
+ = f.select :ticker, options_for_select( @tickers_list, selected: strategy.ticker )
10
+
11
+ .field
12
+ %label slug
13
+ = f.text_field :slug
14
+
15
+ .field
16
+ = f.label :buffer_above_water
17
+ = f.number_field :buffer_above_water, placeholder: "0.49", step: 0.01
18
+ .field
19
+ = f.label :next_max_delta
20
+ = f.number_field :next_max_delta, placeholder: "0.25", step: 0.01
21
+ .field
22
+ = f.label :next_min_strike
23
+ = f.number_field :next_min_strike, placeholder: "20.0", step: 0.01
24
+ .field
25
+ = f.label :threshold_delta
26
+ = f.number_field :threshold_delta, placeholder: "0.14", step: 0.01
27
+ .field
28
+ = f.label :threshold_netp
29
+ = f.number_field :threshold_netp, placeholder: "0.69", step: 0.01
30
+ -# .field
31
+ -# = f.label :current_underlying_strike
32
+ -# = f.number_field :current_underlying_strike, placeholder: "16.66", step: 0.01
33
+
34
+ .actions
35
+ = f.submit
36
+
37
+
@@ -0,0 +1,8 @@
1
+
2
+ = link_to "Strategies (#{Iro::Strategy.all.length})", strategies_path
3
+ .inline-search
4
+ = form_tag strategies_path, method: :get do
5
+ = text_field_tag :q
6
+ = link_to '[+]', new_strategy_path
7
+
8
+
@@ -0,0 +1,18 @@
1
+
2
+ .strategies--show.modal-absolute.hide
3
+ .header
4
+ Strategy
5
+ = link_to '[~]', edit_strategy_path(strategy)
6
+ = strategy
7
+ %ul
8
+ %li <b>ticker:</b> #{strategy.ticker}
9
+ %li <b>slug:</b> #{strategy.slug}
10
+
11
+ %li <b>buffer_above_water:</b> #{strategy.buffer_above_water}
12
+
13
+ %li <b>next_max_delta:</b> #{strategy.next_max_delta}
14
+ %li <b>next_min_strike:</b> #{strategy.next_min_strike}
15
+
16
+ %li <b>threshold_delta:</b> #{strategy.threshold_delta}
17
+ %li <b>threshold_netp:</b> #{strategy.threshold_netp}
18
+
@@ -0,0 +1,18 @@
1
+
2
+ - strategies ||= @strategies
3
+
4
+ .strategies--table
5
+
6
+ %table.bordered
7
+ %tr
8
+ %th &nbsp;
9
+ %th ticker
10
+ %th slug
11
+ %th next_max_delta
12
+ - strategies.each do |sss|
13
+ %tr
14
+ %td= link_to '[~]', edit_strategy_path(sss)
15
+ %td= sss.ticker
16
+ %td= sss.slug
17
+ %td= sss.next_max_delta
18
+
@@ -0,0 +1,3 @@
1
+
2
+ .strategies-edit.maxwidth
3
+ = render 'form', strategy: @strategy
@@ -0,0 +1,3 @@
1
+
2
+ .maxwidth
3
+ = render 'form', strategy: @strategy
data/config/routes.rb CHANGED
@@ -8,7 +8,13 @@ Iro::Engine.routes.draw do
8
8
  get 'datapoints', to: '/iro/datapoints#index'
9
9
 
10
10
  resources :option_watches
11
+
12
+ post 'positions/:id/roll', to: 'positions#roll', as: :roll_position
13
+ resources :positions
11
14
  resources :profiles
15
+ resources :purses
16
+
12
17
  resources :stocks
18
+ resources :strategies
13
19
 
14
20
  end
data/lib/iron_warbler.rb CHANGED
@@ -1,4 +1,8 @@
1
1
 
2
+ require 'business_time'
3
+
2
4
  require 'haml'
5
+
3
6
  require 'mongoid'
7
+
4
8
  require "iro/engine"
@@ -1,6 +1,41 @@
1
1
 
2
2
  namespace :iro do
3
3
 
4
+ desc 'recommend position actions'
5
+ task recommend_position_actions: :environment do
6
+ Iro::Position.active.where({ kind: 'covered_call' }).map &:should_roll?
7
+ end
8
+
9
+ desc 'refresh positions'
10
+ task refresh_positions: :environment do
11
+ Iro::Position.active.where({ kind: 'covered_call' }).map &:refresh
12
+ end
13
+
14
+ desc 'watch positions'
15
+ task watch_positions: :environment do
16
+ while true
17
+ if in_business
18
+
19
+ positions = Iro::Position.active.where({ kind: 'covered_call' })
20
+ positions.each do |position|
21
+ out = Tda::Option.get_quote({
22
+ contractType: 'CALL',
23
+ strike: position.strike,
24
+ expirationDate: position.expires_on,
25
+ ticker: position.ticker,
26
+ })
27
+ position.update({
28
+ end_delta: out[:delta],
29
+ end_price: out[:last],
30
+ })
31
+ end
32
+
33
+ print '.'
34
+ end
35
+ sleep 60 # seconds
36
+ end
37
+ end
38
+
4
39
  desc 'watch stocks'
5
40
  task watch_stocks: :environment do
6
41
  while true