wco_models 3.1.0.240 → 3.1.0.241
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.
- checksums.yaml +4 -4
- data/app/assets/stylesheets/wco/chip.scss +2 -0
- data/app/assets/stylesheets/wco/main.scss +11 -0
- data/app/controllers/wco/leads_controller.rb +37 -0
- data/app/controllers/wco/profiles_controller.rb +6 -0
- data/app/helpers/wco/application_helper.rb +1 -1
- data/app/mailers/iro/alert_mailer.rb +12 -0
- data/app/models/ability.rb +1 -1
- data/app/models/iro/datapoint.rb +17 -15
- data/app/models/iro/option.rb +36 -9
- data/app/models/iro/position.rb +117 -51
- data/app/models/iro/purse.rb +4 -3
- data/app/models/iro/stock.rb +31 -0
- data/app/models/iro/strategy.rb +94 -47
- data/app/models/tda/option.rb +100 -28
- data/app/models/tda/order.rb +135 -0
- data/app/models/tda/stock.rb +1 -2
- data/app/models/wco/office_action_template.rb +2 -1
- data/app/models/wco/profile.rb +10 -0
- data/app/models/wco/tag.rb +2 -1
- data/app/models/wco_email/email_filter.rb +15 -10
- data/app/models/wco_email/email_filter_condition.rb +5 -5
- data/app/models/wco_email/email_template.rb +2 -0
- data/app/models/wco_hosting/appliance_tmpl.rb +8 -3
- data/app/views/wco/leads/_header.haml +2 -1
- data/app/views/wco/leads/_table.haml +5 -3
- data/app/views/wco/leads/new.haml +0 -3
- data/app/views/wco/leads/new_import.haml +20 -0
- data/app/views/wco/profiles/_form.haml +12 -0
- data/app/views/wco/profiles/_header.haml +1 -0
- data/app/views/wco/reports/show.haml +15 -13
- data/app/views/wco/tags/show.haml +7 -1
- data/config/routes.rb +2 -1
- data/lib/wco_models.rb +1 -1
- metadata +5 -3
- data/app/views/wco/leads/_form_import.haml +0 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 992dd5056deab4631dd31cda86e3acd76e266b6d4b4e35e1649151f7fc612e81
|
|
4
|
+
data.tar.gz: b966eb486db7625e6438587694021ad0f8bf72e959fb77e4daaca336ad9df877
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2b84c1ae7b5dd1d0ef0e15557fb50a46b08b3d6bfbb17991e35fe1e429f7069c8b9b6b236df86ca56897d2d4ad7b7a1a3cc72246f3f008548b5d7e45300e6d43
|
|
7
|
+
data.tar.gz: ed56f590661d865a96840b1736f9b3e5ae451ec6f3b7a501a1a804da148ff12fdaecd54301d609b2b776832b975ad36fbf65bfbf612b7bec8a4395e60a72a4c2
|
|
@@ -86,7 +86,10 @@ textarea.monospace {
|
|
|
86
86
|
|
|
87
87
|
.descr {
|
|
88
88
|
border: 1px solid red;
|
|
89
|
+
border-radius: 0.5em;
|
|
90
|
+
|
|
89
91
|
padding: 0.5em;
|
|
92
|
+
margin-bottom: 0.5em;
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
/* E */
|
|
@@ -167,6 +170,14 @@ textarea.monospace {
|
|
|
167
170
|
display: none !important;
|
|
168
171
|
}
|
|
169
172
|
|
|
173
|
+
/* I */
|
|
174
|
+
|
|
175
|
+
input[type=file] {
|
|
176
|
+
margin: 0.5em 0;
|
|
177
|
+
border: 2px solid white;
|
|
178
|
+
border-radius: 0.5em;
|
|
179
|
+
padding: 0.5em;
|
|
180
|
+
}
|
|
170
181
|
|
|
171
182
|
/* M */
|
|
172
183
|
|
|
@@ -26,6 +26,38 @@ class Wco::LeadsController < Wco::ApplicationController
|
|
|
26
26
|
redirect_to action: :index
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
def create_import
|
|
30
|
+
authorize! :create, Wco::Lead
|
|
31
|
+
file = params[:file]
|
|
32
|
+
selected_tag_ids = params[:tags] || []
|
|
33
|
+
|
|
34
|
+
if file.nil?
|
|
35
|
+
redirect_back fallback_location: root_path, alert: "No file selected" and return
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
CSV.foreach(file.path, headers: true) do |row|
|
|
39
|
+
lead_attrs = {
|
|
40
|
+
email: row['email'] || row['Email'],
|
|
41
|
+
name: row['name'] || row['Name'],
|
|
42
|
+
phone: row['phone'] || row['Phone'],
|
|
43
|
+
address: row['address'] || row['Address']
|
|
44
|
+
}.compact ## skip missing columns
|
|
45
|
+
|
|
46
|
+
lead = Wco::Lead.find_by( email: lead_attrs[:email] ) rescue nil
|
|
47
|
+
lead ||= Wco::Lead.create!(lead_attrs)
|
|
48
|
+
|
|
49
|
+
# Assign selected tags
|
|
50
|
+
selected_tag_ids.each do |tag_id|
|
|
51
|
+
lead.tags << Wco::Tag.find(tag_id)
|
|
52
|
+
end
|
|
53
|
+
# lead.save!
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
redirect_to wco.leads_path, notice: "Leads imported successfully"
|
|
57
|
+
rescue => e
|
|
58
|
+
redirect_back fallback_location: root_path, alert: "Error: #{e.message}"
|
|
59
|
+
end
|
|
60
|
+
|
|
29
61
|
def edit
|
|
30
62
|
authorize! :edit, Wco::Lead
|
|
31
63
|
@lead = Wco::Lead.find params[:id]
|
|
@@ -48,6 +80,7 @@ class Wco::LeadsController < Wco::ApplicationController
|
|
|
48
80
|
end
|
|
49
81
|
end
|
|
50
82
|
|
|
83
|
+
@leads = @leads.includes( :tags )
|
|
51
84
|
@leads = @leads.page( params[:leads_page ] ).per( current_profile.per_page )
|
|
52
85
|
end
|
|
53
86
|
|
|
@@ -56,6 +89,10 @@ class Wco::LeadsController < Wco::ApplicationController
|
|
|
56
89
|
@lead = Wco::Lead.new
|
|
57
90
|
end
|
|
58
91
|
|
|
92
|
+
def new_import
|
|
93
|
+
authorize! :new, Wco::Lead
|
|
94
|
+
end
|
|
95
|
+
|
|
59
96
|
def show
|
|
60
97
|
@lead = Wco::Lead.where({ id: params[:id] }).first
|
|
61
98
|
@lead ||= Wco::Lead.where({ email: params[:id] }).first
|
|
@@ -24,6 +24,12 @@ class Wco::ProfilesController < Wco::ApplicationController
|
|
|
24
24
|
if params[:q]
|
|
25
25
|
q = URI.decode(params[:q])
|
|
26
26
|
@profiles = @profiles.where({ email: /#{q}/i })
|
|
27
|
+
|
|
28
|
+
if params[:q] == 'pi'
|
|
29
|
+
profile = Wco::Profile.find_by email: 'piousbox@gmail.com'
|
|
30
|
+
redirect_to action: 'edit', id: profile.id.to_s
|
|
31
|
+
return
|
|
32
|
+
end
|
|
27
33
|
end
|
|
28
34
|
end
|
|
29
35
|
|
|
@@ -42,7 +42,7 @@ module Wco::ApplicationHelper
|
|
|
42
42
|
def pp_money a; pp_amount a; end
|
|
43
43
|
def pp_currency a; pp_amount a; end
|
|
44
44
|
def pp_percent a, config = { precision: 2}
|
|
45
|
-
"#{(a*100).round( config[:precision] )}%" rescue '
|
|
45
|
+
"#{(a*100).round( config[:precision] )}%" rescue 'nil@#pp_percent'
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
|
|
2
|
+
class Iro::AlertMailer < ActionMailer::Base
|
|
3
|
+
default from: 'no-reply@wasya.co'
|
|
4
|
+
layout 'mailer'
|
|
5
|
+
|
|
6
|
+
def stock_alert id
|
|
7
|
+
@alert = Iro::Alert.find id
|
|
8
|
+
mail( to: 'victor@piousbox.com',
|
|
9
|
+
subject: "#{Time.now.to_date} Iro::AlertMailer#stock_alert" )
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
end
|
data/app/models/ability.rb
CHANGED
|
@@ -10,7 +10,7 @@ class Ability
|
|
|
10
10
|
|
|
11
11
|
if user
|
|
12
12
|
|
|
13
|
-
if [ 'piousbox@gmail.com', 'victor@piousbox.com', '
|
|
13
|
+
if [ 'piousbox@gmail.com', 'victor@piousbox.com', 'victor@wasya.co' ].include? user.email
|
|
14
14
|
can :manage, :all
|
|
15
15
|
end
|
|
16
16
|
|
data/app/models/iro/datapoint.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require 'csv'
|
|
1
2
|
|
|
2
3
|
##
|
|
3
4
|
## Datapoints are at most daily!
|
|
@@ -42,20 +43,21 @@ class Iro::Datapoint
|
|
|
42
43
|
|
|
43
44
|
field :date, type: Date
|
|
44
45
|
index({ kind: -1, date: -1 })
|
|
45
|
-
validates :date, uniqueness: { scope: [ :symbol ] }
|
|
46
|
+
validates :date, uniqueness: { scope: [ :kind, :symbol ] }
|
|
46
47
|
|
|
48
|
+
## @obsolete, @deprecated, use :date instead?
|
|
47
49
|
field :quote_at, type: DateTime
|
|
48
50
|
index({ kind: -1, quote_at: -1 })
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
field :open, type: Float
|
|
52
|
-
field :high, type: Float
|
|
53
|
-
field :low, type: Float
|
|
54
|
-
def close; value; end
|
|
55
|
-
def close= a; value= a; end
|
|
51
|
+
## I don't understand why this was forced to be unique... I want the date to be unique.
|
|
52
|
+
# validates :quote_at, uniqueness: { scope: [ :kind, :symbol ] } ## scope-by-kind is unnecessary here? _vp_ 2024-08-08
|
|
56
53
|
|
|
54
|
+
field :open, type: Float
|
|
55
|
+
field :high, type: Float
|
|
56
|
+
field :low, type: Float
|
|
57
57
|
field :value, type: Float
|
|
58
58
|
validates :value, presence: true
|
|
59
|
+
def close; value; end
|
|
60
|
+
def close= a; value= a; end
|
|
59
61
|
|
|
60
62
|
|
|
61
63
|
field :volume, type: Integer
|
|
@@ -165,15 +167,15 @@ class Iro::Datapoint
|
|
|
165
167
|
flag = create({
|
|
166
168
|
kind: KIND_STOCK,
|
|
167
169
|
symbol: symbol,
|
|
168
|
-
date: row['
|
|
169
|
-
quote_at: row['
|
|
170
|
+
date: row['date'],
|
|
171
|
+
quote_at: row['date'],
|
|
170
172
|
|
|
171
|
-
volume: row['
|
|
173
|
+
volume: row['volume'],
|
|
172
174
|
|
|
173
|
-
open: row['
|
|
174
|
-
high: row['
|
|
175
|
-
low: row['
|
|
176
|
-
value: row['
|
|
175
|
+
open: row['open'],
|
|
176
|
+
high: row['high'],
|
|
177
|
+
low: row['low'],
|
|
178
|
+
value: row['close'],
|
|
177
179
|
})
|
|
178
180
|
if flag.persisted?
|
|
179
181
|
print '^'
|
data/app/models/iro/option.rb
CHANGED
|
@@ -16,7 +16,8 @@ class Iro::Option
|
|
|
16
16
|
CALL = 'CALL'
|
|
17
17
|
PUT = 'PUT'
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
## for now, recompute every time
|
|
20
|
+
# field :symbol
|
|
20
21
|
## each option can be a leg in a position, no uniqueness
|
|
21
22
|
# validates :symbol, uniqueness: true, presence: true
|
|
22
23
|
|
|
@@ -28,6 +29,11 @@ class Iro::Option
|
|
|
28
29
|
field :strike, type: :float
|
|
29
30
|
validates :strike, presence: true
|
|
30
31
|
|
|
32
|
+
def to_s
|
|
33
|
+
"#{symbol} :: #{expires_on.strftime('%Y-%m-%d')} #{put_call} #{strike}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
|
|
31
37
|
field :expires_on, type: :date
|
|
32
38
|
validates :expires_on, presence: true
|
|
33
39
|
def self.expirations_list full: false, n: 5
|
|
@@ -59,13 +65,14 @@ class Iro::Option
|
|
|
59
65
|
field :end_delta, type: :float
|
|
60
66
|
|
|
61
67
|
|
|
62
|
-
has_one :
|
|
63
|
-
has_one :
|
|
68
|
+
has_one :pos_of_outer, class_name: 'Iro::Position', inverse_of: :outer
|
|
69
|
+
has_one :pos_of_inner, class_name: 'Iro::Position', inverse_of: :inner
|
|
64
70
|
|
|
65
71
|
field :last, type: :float
|
|
66
72
|
|
|
67
73
|
## for TDA
|
|
68
|
-
|
|
74
|
+
## "COST_030626C1030"
|
|
75
|
+
def symbol_old
|
|
69
76
|
if !self[:symbol]
|
|
70
77
|
p_c_ = put_call == 'PUT' ? 'P' : 'C'
|
|
71
78
|
strike_ = strike.to_i == strike ? strike.to_i : strike
|
|
@@ -76,18 +83,38 @@ class Iro::Option
|
|
|
76
83
|
self[:symbol]
|
|
77
84
|
end
|
|
78
85
|
|
|
79
|
-
|
|
86
|
+
## for schwab
|
|
87
|
+
## "COST 260306C01030000"
|
|
88
|
+
def symbol
|
|
89
|
+
p_c_ = put_call == 'PUT' ? 'P' : 'C'
|
|
90
|
+
strike_ = format("%08d", (strike.to_f * 1000).round)
|
|
91
|
+
sym = "#{stock.ticker.ljust(6)}#{expires_on.strftime("%y%m%d")}#{p_c_}#{strike_}"
|
|
92
|
+
end
|
|
93
|
+
=begin
|
|
94
|
+
def symbol_trash ## it persists - which I dont do right now
|
|
95
|
+
if !self[:symbol]
|
|
96
|
+
p_c_ = put_call == 'PUT' ? 'P' : 'C'
|
|
97
|
+
strike_ = format("%08d", (strike.to_f * 1000).round)
|
|
98
|
+
sym = "#{stock.ticker.ljust(6)}#{expires_on.strftime("%y%m%d")}#{p_c_}#{strike_}"
|
|
99
|
+
self[:symbol] = sym
|
|
100
|
+
save
|
|
101
|
+
end
|
|
102
|
+
self[:symbol]
|
|
103
|
+
end
|
|
104
|
+
=end
|
|
105
|
+
|
|
106
|
+
# before_save :sync, if: ->() { !Rails.env.test? } ## do not sync in test
|
|
80
107
|
def sync
|
|
81
108
|
out = Tda::Option.get_quote({
|
|
82
109
|
contractType: put_call,
|
|
83
110
|
strike: strike,
|
|
84
|
-
expirationDate: expires_on,
|
|
111
|
+
expirationDate: expires_on.strftime('%Y-%m-%d'),
|
|
85
112
|
ticker: ticker,
|
|
86
113
|
})
|
|
87
|
-
puts! out,
|
|
114
|
+
puts! out, "option sync of `#{self.to_s}`"
|
|
88
115
|
self.end_price = ( out.bid + out.ask ) / 2 rescue 0
|
|
89
|
-
self.end_delta = out.delta
|
|
90
|
-
|
|
116
|
+
self.end_delta = out.delta ? out.delta : 0.0
|
|
117
|
+
self.save! ## 2026-02-19 this must be present.
|
|
91
118
|
end
|
|
92
119
|
|
|
93
120
|
def self.max_pain hash
|
data/app/models/iro/position.rb
CHANGED
|
@@ -5,64 +5,71 @@ class Iro::Position
|
|
|
5
5
|
include Mongoid::Paranoia
|
|
6
6
|
store_in collection: 'iro_positions'
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def prev_gain_loss_amount
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
end
|
|
8
|
+
## @trash, use next_gain_loss_amount instead
|
|
9
|
+
# field :prev_gain_loss_amount, type: :float
|
|
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
|
|
14
|
+
field :next_gain_loss_amount, type: :float
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
STATUS_ACTIVE = 'active'
|
|
17
18
|
STATUS_CLOSED = 'closed'
|
|
19
|
+
STATUS_PREPARE = 'prepare'
|
|
18
20
|
STATUS_PROPOSED = 'proposed'
|
|
19
21
|
## one more, 'selected' after proposed?
|
|
20
22
|
STATUS_PENDING = 'pending' ## 'working'
|
|
21
|
-
STATUSES = [ nil, STATUS_CLOSED, STATUS_ACTIVE, STATUS_PROPOSED, STATUS_PENDING ]
|
|
23
|
+
STATUSES = [ nil, STATUS_CLOSED, STATUS_ACTIVE, STATUS_PREPARE, STATUS_PROPOSED, STATUS_PENDING ]
|
|
22
24
|
field :status
|
|
23
25
|
validates :status, presence: true
|
|
24
26
|
scope :active, ->{ where( status: 'active' ) }
|
|
27
|
+
field :schwab_status
|
|
25
28
|
|
|
26
|
-
belongs_to :purse,
|
|
29
|
+
belongs_to :purse, class_name: 'Iro::Purse', inverse_of: :positions
|
|
27
30
|
index({ purse_id: 1, ticker: 1 })
|
|
28
31
|
|
|
29
|
-
belongs_to :stock,
|
|
32
|
+
belongs_to :stock, class_name: 'Iro::Stock', inverse_of: :positions
|
|
30
33
|
delegate :ticker, to: :stock
|
|
31
34
|
|
|
32
35
|
belongs_to :strategy, class_name: 'Iro::Strategy', inverse_of: :positions
|
|
36
|
+
delegate :long_or_short, to: :strategy
|
|
37
|
+
delegate :credit_or_debit, to: :strategy
|
|
33
38
|
|
|
34
|
-
## no: the strategy can be wheel, and position is put-spread.
|
|
35
|
-
# delegate :put_call, to: :strategy
|
|
36
39
|
field :put_call, type: :string
|
|
37
40
|
validates :put_call, presence: true
|
|
41
|
+
def put_call
|
|
42
|
+
self[:put_call] || self.strategy.put_call
|
|
43
|
+
end
|
|
38
44
|
|
|
39
|
-
delegate :long_or_short, to: :strategy
|
|
40
|
-
delegate :credit_or_debit, to: :strategy
|
|
41
45
|
|
|
42
46
|
belongs_to :next_strategy, class_name: 'Iro::Strategy', inverse_of: :next_position, optional: true
|
|
43
47
|
|
|
44
48
|
|
|
45
|
-
belongs_to :prev, class_name: 'Iro::Position', inverse_of: :nxts, optional: true
|
|
46
|
-
belongs_to :autoprev, class_name: 'Iro::Position', inverse_of: :autonxt, optional: true
|
|
47
49
|
## there are many of these, for viewing on the 'roll' view
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
belongs_to :prev, class_name: 'Iro::Position', inverse_of: :nxts, optional: true
|
|
51
|
+
has_many :nxts, class_name: 'Iro::Position', inverse_of: :prev
|
|
52
|
+
|
|
53
|
+
## 2026-02-26 using this one.
|
|
54
|
+
belongs_to :autonxt, class_name: 'Iro::Position', inverse_of: :autoprev, optional: true
|
|
55
|
+
has_one :autoprev, class_name: 'Iro::Position', inverse_of: :autonxt
|
|
56
|
+
|
|
50
57
|
|
|
51
58
|
## Options
|
|
52
59
|
|
|
53
|
-
belongs_to :inner, class_name: 'Iro::Option', inverse_of: :
|
|
60
|
+
belongs_to :inner, class_name: 'Iro::Option', inverse_of: :pos_of_inner
|
|
54
61
|
validates_associated :inner
|
|
55
62
|
|
|
56
|
-
belongs_to :outer, class_name: 'Iro::Option', inverse_of: :
|
|
63
|
+
belongs_to :outer, class_name: 'Iro::Option', inverse_of: :pos_of_outer, optional: true
|
|
57
64
|
validates_associated :outer
|
|
58
65
|
|
|
59
66
|
accepts_nested_attributes_for :inner, :outer
|
|
60
67
|
|
|
61
68
|
field :outer_strike, type: :float
|
|
62
|
-
|
|
69
|
+
validates :outer_strike, presence: true ## 2026-02-24 only to make finding easier.
|
|
63
70
|
|
|
64
71
|
field :inner_strike, type: :float
|
|
65
|
-
|
|
72
|
+
validates :inner_strike, presence: true ## 2026-02-24 only to make finding easier.
|
|
66
73
|
|
|
67
74
|
field :expires_on
|
|
68
75
|
validates :expires_on, presence: true
|
|
@@ -75,6 +82,8 @@ class Iro::Position
|
|
|
75
82
|
|
|
76
83
|
field :end_on
|
|
77
84
|
|
|
85
|
+
field :schwab_order_id, type: :integer
|
|
86
|
+
|
|
78
87
|
def begin_delta
|
|
79
88
|
strategy.send("begin_delta_#{strategy.kind}", self)
|
|
80
89
|
end
|
|
@@ -83,9 +92,28 @@ class Iro::Position
|
|
|
83
92
|
end
|
|
84
93
|
|
|
85
94
|
def breakeven
|
|
86
|
-
|
|
95
|
+
send("breakeven_#{strategy.kind}")
|
|
96
|
+
end
|
|
97
|
+
def breakeven_covered_call
|
|
98
|
+
p = self
|
|
99
|
+
p.inner.strike + p.inner.begin_price
|
|
100
|
+
end
|
|
101
|
+
def breakeven_long_debit_call_spread
|
|
102
|
+
p = self
|
|
103
|
+
p.inner.strike - p.max_gain
|
|
104
|
+
end
|
|
105
|
+
## 2026-02-23
|
|
106
|
+
def breakeven_short_credit_call_spread
|
|
107
|
+
p = self
|
|
108
|
+
p.inner.strike + p.max_gain
|
|
109
|
+
end
|
|
110
|
+
## 2026-02-23
|
|
111
|
+
def breakeven_long_credit_put_spread
|
|
112
|
+
p = self
|
|
113
|
+
p.inner.strike - p.max_gain
|
|
87
114
|
end
|
|
88
115
|
|
|
116
|
+
|
|
89
117
|
def current_underlying_strike
|
|
90
118
|
Iro::Stock.find_by( ticker: ticker ).last
|
|
91
119
|
end
|
|
@@ -104,16 +132,30 @@ class Iro::Position
|
|
|
104
132
|
print '^'
|
|
105
133
|
end
|
|
106
134
|
|
|
135
|
+
def roll_price
|
|
136
|
+
pos = self
|
|
137
|
+
out = pos.autoprev.outer.end_price - pos.autoprev.inner.end_price + pos.inner.begin_price - pos.outer.begin_price
|
|
138
|
+
return out.round(2)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
|
|
107
142
|
def net_percent
|
|
108
143
|
net_amount / max_gain
|
|
109
144
|
end
|
|
110
145
|
def net_amount # each
|
|
111
146
|
self.send("net_amount_#{strategy.kind}")
|
|
112
147
|
end
|
|
148
|
+
def net_amount_covered_call
|
|
149
|
+
inner.begin_price - inner.end_price
|
|
150
|
+
end
|
|
113
151
|
## 2025-10-14 tested
|
|
114
152
|
def net_amount_long_credit_put_spread ## each
|
|
115
153
|
inner.begin_price - outer.begin_price + outer.end_price - inner.end_price
|
|
116
154
|
end
|
|
155
|
+
## 2026-02-19 tested
|
|
156
|
+
def net_amount_short_credit_call_spread
|
|
157
|
+
return net_amount_long_credit_put_spread
|
|
158
|
+
end
|
|
117
159
|
|
|
118
160
|
def max_gain # each
|
|
119
161
|
strategy.send("max_gain_#{strategy.kind}", self)
|
|
@@ -124,6 +166,18 @@ class Iro::Position
|
|
|
124
166
|
|
|
125
167
|
|
|
126
168
|
def sync
|
|
169
|
+
if schwab_order_id
|
|
170
|
+
outs = Tda::Order.check_status schwab_order_id
|
|
171
|
+
update({ schwab_status: outs['status'] })
|
|
172
|
+
if [ Tda::Order::STATUS_FILLED, Tda::Order::STATUS_REPLACED ].include?( outs['status'] )
|
|
173
|
+
## update amounts.
|
|
174
|
+
purse.update({ available_amount: purse.available_amount + next_gain_loss_amount*quantity*100 })
|
|
175
|
+
## make this one active
|
|
176
|
+
update({ status: Iro::Position::STATUS_ACTIVE, next_gain_loss_amount: nil })
|
|
177
|
+
## make previous one closed
|
|
178
|
+
autoprev.update({ status: Iro::Position::STATUS_CLOSED })
|
|
179
|
+
end
|
|
180
|
+
end
|
|
127
181
|
inner.sync
|
|
128
182
|
outer.sync
|
|
129
183
|
end
|
|
@@ -138,26 +192,31 @@ class Iro::Position
|
|
|
138
192
|
|
|
139
193
|
## should_roll?
|
|
140
194
|
def calc_rollp
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
#
|
|
195
|
+
pos = self
|
|
196
|
+
pos.next_reasons = []
|
|
197
|
+
# pos.next_symbol = nil
|
|
198
|
+
# pos.next_delta = nil
|
|
144
199
|
|
|
145
|
-
out = strategy.send(
|
|
200
|
+
out = strategy.send("calc_rollp_#{strategy.kind}", pos )
|
|
146
201
|
|
|
147
|
-
|
|
148
|
-
|
|
202
|
+
pos.rollp = out[0]
|
|
203
|
+
pos.next_reasons.push out[1]
|
|
149
204
|
save
|
|
150
205
|
end
|
|
151
206
|
|
|
152
207
|
def calc_nxt
|
|
153
208
|
pos = self
|
|
209
|
+
puts! pos, '#calc_nxt...'
|
|
154
210
|
|
|
155
|
-
## 7 days ahead - not configurable
|
|
156
|
-
|
|
211
|
+
## 7 days ahead - not configurable
|
|
212
|
+
params = {
|
|
157
213
|
contractType: pos.put_call,
|
|
158
214
|
expirationDate: next_expires_on,
|
|
159
215
|
ticker: ticker,
|
|
160
|
-
}
|
|
216
|
+
}
|
|
217
|
+
puts! params, 'ze params'
|
|
218
|
+
outs = Tda::Option.get_quotes(params)
|
|
219
|
+
puts! outs, 'outs'
|
|
161
220
|
outs_bk = outs.dup
|
|
162
221
|
|
|
163
222
|
outs = outs.select do |out|
|
|
@@ -169,7 +228,7 @@ class Iro::Position
|
|
|
169
228
|
elsif 'PUT' == pos.put_call
|
|
170
229
|
outs = outs.reverse
|
|
171
230
|
end
|
|
172
|
-
puts! outs, '#calc_nxt.outs -> 2'
|
|
231
|
+
# puts! outs, '#calc_nxt.outs -> 2'
|
|
173
232
|
|
|
174
233
|
## next_inner_strike
|
|
175
234
|
outs = outs.select do |out|
|
|
@@ -186,7 +245,7 @@ class Iro::Position
|
|
|
186
245
|
end
|
|
187
246
|
end
|
|
188
247
|
puts! outs[0][:strikePrice], 'after calc next_inner_strike'
|
|
189
|
-
puts! outs, 'outs'
|
|
248
|
+
# puts! outs, 'outs'
|
|
190
249
|
|
|
191
250
|
## next_buffer_above_water
|
|
192
251
|
outs = outs.select do |out|
|
|
@@ -232,37 +291,44 @@ class Iro::Position
|
|
|
232
291
|
put_call: pos.put_call,
|
|
233
292
|
stock_id: pos.stock_id,
|
|
234
293
|
}
|
|
235
|
-
|
|
294
|
+
inner_attrs = o_attrs.merge({
|
|
236
295
|
strike: inner[:strikePrice],
|
|
237
296
|
begin_price: ( inner[:bid] + inner[:ask] )/2,
|
|
238
297
|
begin_delta: inner[:delta],
|
|
239
298
|
end_price: ( inner[:bid] + inner[:ask] )/2,
|
|
240
299
|
end_delta: inner[:delta],
|
|
241
|
-
})
|
|
242
|
-
|
|
300
|
+
})
|
|
301
|
+
outer_attrs = o_attrs.merge({
|
|
243
302
|
strike: outer[:strikePrice],
|
|
244
303
|
begin_price: ( outer[:bid] + outer[:ask] )/2,
|
|
245
304
|
begin_delta: outer[:delta],
|
|
246
305
|
end_price: ( outer[:bid] + outer[:ask] )/2,
|
|
247
306
|
end_delta: outer[:delta],
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
pos.autonxt.update({
|
|
251
|
-
prev_gain_loss_amount: 'a',
|
|
307
|
+
})
|
|
308
|
+
autonxt_attrs = {
|
|
252
309
|
put_call: pos.put_call,
|
|
253
310
|
status: 'proposed',
|
|
254
311
|
stock: strategy.stock,
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
inner_strike: inner_.strike,
|
|
258
|
-
outer_strike: outer_.strike,
|
|
312
|
+
inner_strike: inner_attrs[:strike],
|
|
313
|
+
outer_strike: outer_attrs[:strike],
|
|
259
314
|
begin_on: Time.now.to_date,
|
|
260
315
|
expires_on: next_expires_on,
|
|
261
316
|
purse: purse,
|
|
262
317
|
strategy: strategy,
|
|
263
|
-
quantity:
|
|
318
|
+
quantity: pos.quantity,
|
|
264
319
|
autoprev: pos,
|
|
265
|
-
}
|
|
320
|
+
}
|
|
321
|
+
pos.autonxt ||= Iro::Position.where({
|
|
322
|
+
inner_strike: inner_attrs[:strike],
|
|
323
|
+
outer_strike: outer_attrs[:strike],
|
|
324
|
+
purse: purse,
|
|
325
|
+
stock: strategy.stock,
|
|
326
|
+
strategy: strategy,
|
|
327
|
+
}).first
|
|
328
|
+
pos.autonxt ||= Iro::Position.new(autonxt_attrs)
|
|
329
|
+
pos.autonxt.update(autonxt_attrs)
|
|
330
|
+
pos.autonxt.inner.update(inner_attrs)
|
|
331
|
+
pos.autonxt.outer.update(outer_attrs)
|
|
266
332
|
|
|
267
333
|
pos.autonxt.sync
|
|
268
334
|
pos.autonxt.save!
|
|
@@ -282,7 +348,7 @@ class Iro::Position
|
|
|
282
348
|
if !out.workday?
|
|
283
349
|
out = Time.previous_business_day(out)
|
|
284
350
|
end
|
|
285
|
-
return out
|
|
351
|
+
return out.strftime('%Y-%m-%d')
|
|
286
352
|
end
|
|
287
353
|
|
|
288
354
|
## ok
|
|
@@ -298,14 +364,14 @@ class Iro::Position
|
|
|
298
364
|
def to_s
|
|
299
365
|
out = "#{stock} (#{q}) #{expires_on.to_datetime.strftime('%b %d')} #{strategy.long_or_short} ["
|
|
300
366
|
if Iro::Strategy::LONG == long_or_short
|
|
301
|
-
if outer
|
|
302
|
-
out = out + "$#{outer.strike}
|
|
367
|
+
if outer&.strike
|
|
368
|
+
out = out + "$#{outer.strike} <- "
|
|
303
369
|
end
|
|
304
370
|
out = out + "$#{inner.strike}"
|
|
305
371
|
else
|
|
306
372
|
out = out + "$#{inner.strike}"
|
|
307
|
-
if outer
|
|
308
|
-
out = out + "
|
|
373
|
+
if outer&.strike
|
|
374
|
+
out = out + " -> $#{outer.strike}"
|
|
309
375
|
end
|
|
310
376
|
end
|
|
311
377
|
out += "] "
|
data/app/models/iro/purse.rb
CHANGED
|
@@ -29,13 +29,14 @@ class Iro::Purse
|
|
|
29
29
|
field :n_next_positions, type: :integer, default: 5
|
|
30
30
|
|
|
31
31
|
field :available_amount, type: :float
|
|
32
|
+
validates :available_amount, presence: true
|
|
32
33
|
def available
|
|
33
34
|
available_amount
|
|
34
35
|
end
|
|
35
36
|
|
|
36
|
-
def balance
|
|
37
|
-
|
|
38
|
-
end
|
|
37
|
+
# def balance
|
|
38
|
+
# 0.01
|
|
39
|
+
# end
|
|
39
40
|
|
|
40
41
|
def delta_wt_avg( begin_end, long_short, inner_outer )
|
|
41
42
|
max_loss_total = 0
|
data/app/models/iro/stock.rb
CHANGED
|
@@ -26,9 +26,12 @@ class ::Iro::Stock
|
|
|
26
26
|
|
|
27
27
|
field :last, type: :float
|
|
28
28
|
field :options_price_increment, type: :float
|
|
29
|
+
validates :options_price_increment, presence: true
|
|
29
30
|
|
|
30
31
|
field :stdev, type: :float
|
|
31
32
|
|
|
33
|
+
field :descr, type: :string
|
|
34
|
+
|
|
32
35
|
has_many :positions, class_name: '::Iro::Position', inverse_of: :stock
|
|
33
36
|
has_many :strategies, class_name: '::Iro::Strategy', inverse_of: :stock
|
|
34
37
|
# has_many :purses, class_name: '::Iro::Purse', inverse_of: :stock
|
|
@@ -137,5 +140,33 @@ class ::Iro::Stock
|
|
|
137
140
|
# sum_sqr = contents.map {|x| x * x}.reduce(&:+) # => 285.0
|
|
138
141
|
# std_dev = Math.sqrt((sum_sqr - n * mean * mean)/(n-1)) # => 2.7386127875258306
|
|
139
142
|
|
|
143
|
+
##
|
|
144
|
+
## From: stockdata_org
|
|
145
|
+
##
|
|
146
|
+
def get_historic_data date_from=nil, date_to=nil
|
|
147
|
+
date_from ||= Time.now - 1.year - 1.week
|
|
148
|
+
date_to ||= date_from + 180.days
|
|
149
|
+
date_from = date_from.strftime('%Y-%m-%d')
|
|
150
|
+
date_to = date_to.strftime('%Y-%m-%d')
|
|
151
|
+
puts! [ticker, date_from, date_to], "ticker,date_from,date_to"
|
|
152
|
+
outs = HTTParty.get("https://api.stockdata.org/v1/data/eod?symbols=#{ticker}&date_from=#{date_from}&date_to=#{date_to}&api_token=#{STOCKDATA_ORG_KEY}")
|
|
153
|
+
outs['data'].each do |datum|
|
|
154
|
+
existing = ::Iro::Datapoint.find_by({ symbol: ticker, date: datum['date'].to_date.strftime('%Y-%m-%d') }) rescue nil
|
|
155
|
+
if existing
|
|
156
|
+
print('.')
|
|
157
|
+
else
|
|
158
|
+
::Iro::Datapoint.create!({ symbol: ticker,
|
|
159
|
+
kind: ::Iro::Datapoint::KIND_STOCK,
|
|
160
|
+
date: datum['date'].to_date.strftime('%Y-%m-%d'),
|
|
161
|
+
open: datum['open'],
|
|
162
|
+
high: datum['high'],
|
|
163
|
+
low: datum['low'],
|
|
164
|
+
value: datum['close'],
|
|
165
|
+
volume: datum['volume'],
|
|
166
|
+
})
|
|
167
|
+
print('^')
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
140
171
|
|
|
141
172
|
end
|