iron_warbler 2.0.7.23 → 2.0.7.24
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/README.txt +19 -0
- data/app/assets/stylesheets/iron_warbler/purses_gameui.scss +5 -30
- data/app/models/iro/datapoint.rb +7 -7
- data/app/models/iro/option.rb +173 -3
- data/app/models/iro/purse.rb +4 -1
- data/app/models/iro/stock.rb +81 -0
- data/app/models/tda/option.rb +101 -5
- data/app/views/iro/purses/_form_extra_fields.haml +4 -4
- data/app/views/iro/purses/gameui.haml +2 -1
- data/lib/tasks/db_tasks.rake +1 -1
- data/lib/tasks/test_tasks.rake +54 -0
- metadata +18 -4
- data/app/models/iro/trash/position_covered_call.rb +0 -4
- data/app/models/iro/trash/position_debit_spread.rb +0 -251
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1b73ac7c5b7ca0d29fe20cd0db0dcdca4713a710790bff2cffe8c30c3a18d2d2
|
|
4
|
+
data.tar.gz: daef47e2b8841f0253e55d19a8c8703c7aff086c53711b52db3dfdb470a97a78
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e59a49397b934c8537abe40eb23eba846aa770956e02168722b2eb5fd6a1e19581388dc43d3ecb6e211e90cffec5cf8b02ef5eaa6efdcb1f40b4599c4125c8e8
|
|
7
|
+
data.tar.gz: 8f10a4503e57e02d17ca0dd7a310fc73489dfd22fb23e23f16a79de72f3a589790c09e9facbfb2441daf3fcef2b93456bcac12ce7a767c35ce7e29c2ccffc48f
|
data/README.txt
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
|
|
2
|
+
= Install =
|
|
3
|
+
|
|
4
|
+
From: https://docs.galpy.org/en/latest/installation.html
|
|
5
|
+
|
|
6
|
+
brew install gsl
|
|
7
|
+
|
|
8
|
+
= Use =
|
|
9
|
+
|
|
10
|
+
calculator: https://www.omnicalculator.com/finance/black-scholes
|
|
11
|
+
|
|
12
|
+
From: https://pythoninoffice.com/calculate-black-scholes-option-price-in-python/
|
|
13
|
+
|
|
14
|
+
-=---
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
https://api.tdameritrade.com/v1/accounts/232718838/orders/13597943085
|
|
18
|
+
|
|
19
|
+
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
.purses-gameuiW {
|
|
3
3
|
border: 1px solid red;
|
|
4
4
|
width: 100%;
|
|
5
|
-
overflow: scroll;
|
|
5
|
+
// overflow-x: scroll;
|
|
6
|
+
overflow-y: hidden;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
.purses-gameui {
|
|
@@ -78,7 +79,7 @@
|
|
|
78
79
|
bottom: -10%;
|
|
79
80
|
border: 2px dotted green;
|
|
80
81
|
background: rgba(0,255,0, 0.5);
|
|
81
|
-
height:
|
|
82
|
+
height: 0.5em;
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
.sprite {
|
|
@@ -142,32 +143,6 @@
|
|
|
142
143
|
background: rgba(255,0,0, 0.75);
|
|
143
144
|
}
|
|
144
145
|
|
|
145
|
-
|
|
146
|
-
// .grid-mark:nth-child(4n + 1) {
|
|
147
|
-
// border: 1px solid red;
|
|
148
|
-
// display: none;
|
|
149
|
-
// }
|
|
150
|
-
// .grid-mark:nth-child(4n) {
|
|
151
|
-
// border: 1px solid red;
|
|
152
|
-
// display: none;
|
|
153
|
-
// }
|
|
154
|
-
// .grid-mark:nth-child(6n + 1) {
|
|
155
|
-
// border: 1px solid red;
|
|
156
|
-
// display: none;
|
|
157
|
-
// }
|
|
158
|
-
// .grid-mark:nth-child(6n) {
|
|
159
|
-
// border: 1px solid red;
|
|
160
|
-
// display: none;
|
|
161
|
-
// }
|
|
162
|
-
// .grid-mark:nth-child(3n + 1) {
|
|
163
|
-
// border: 1px solid red;
|
|
164
|
-
// display: none;
|
|
165
|
-
// }
|
|
166
|
-
// .grid-mark:nth-child(3n) {
|
|
167
|
-
// border: 1px solid red;
|
|
168
|
-
// display: none;
|
|
169
|
-
// }
|
|
170
|
-
|
|
171
146
|
.grid-mark {
|
|
172
147
|
// border: 1px solid red;
|
|
173
148
|
|
|
@@ -206,8 +181,8 @@
|
|
|
206
181
|
.labelC {
|
|
207
182
|
background: white;
|
|
208
183
|
position: absolute;
|
|
209
|
-
|
|
210
|
-
|
|
184
|
+
bottom: -1em;
|
|
185
|
+
right: 0;
|
|
211
186
|
}
|
|
212
187
|
}
|
|
213
188
|
}
|
data/app/models/iro/datapoint.rb
CHANGED
|
@@ -138,15 +138,15 @@ class Iro::Datapoint
|
|
|
138
138
|
flag = create({
|
|
139
139
|
kind: 'STOCK',
|
|
140
140
|
symbol: symbol,
|
|
141
|
-
date: row['
|
|
142
|
-
quote_at: row['
|
|
141
|
+
date: row['Date'],
|
|
142
|
+
quote_at: row['Date'],
|
|
143
143
|
|
|
144
|
-
volume: row['
|
|
144
|
+
volume: row['Volume'],
|
|
145
145
|
|
|
146
|
-
open: row['
|
|
147
|
-
high: row['
|
|
148
|
-
low: row['
|
|
149
|
-
value: row['
|
|
146
|
+
open: row['Open'],
|
|
147
|
+
high: row['High'],
|
|
148
|
+
low: row['Low'],
|
|
149
|
+
value: row['Close'],
|
|
150
150
|
})
|
|
151
151
|
print '.' if flag.persisted?
|
|
152
152
|
end
|
data/app/models/iro/option.rb
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
require 'distribution'
|
|
2
|
+
N = Distribution::Normal
|
|
3
|
+
# include Math
|
|
4
|
+
# require 'business_time'
|
|
2
5
|
|
|
3
6
|
class Iro::Option
|
|
4
7
|
include Mongoid::Document
|
|
@@ -6,11 +9,178 @@ class Iro::Option
|
|
|
6
9
|
include Mongoid::Paranoia
|
|
7
10
|
store_in collection: 'iro_options'
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
attr_accessor :recompute
|
|
13
|
+
|
|
14
|
+
field :ticker
|
|
15
|
+
validates :ticker, presence: true
|
|
11
16
|
|
|
12
17
|
field :symbol
|
|
13
18
|
validates :symbol, uniqueness: true, presence: true
|
|
14
19
|
|
|
20
|
+
field :put_call, type: :string
|
|
21
|
+
validates :put_call, presence: true
|
|
22
|
+
|
|
23
|
+
field :delta, type: :float
|
|
24
|
+
|
|
25
|
+
field :strike, type: :float
|
|
26
|
+
validates :strike, presence: true
|
|
27
|
+
|
|
28
|
+
field :expires_on, type: :date
|
|
29
|
+
validates :expires_on, presence: true
|
|
30
|
+
|
|
31
|
+
belongs_to :stock, class_name: 'Iro::Stock', inverse_of: :strategies
|
|
32
|
+
|
|
33
|
+
def symbol
|
|
34
|
+
if !self[:symbol]
|
|
35
|
+
p_c_ = put_call == 'PUT' ? 'P' : 'C'
|
|
36
|
+
strike_ = strike.to_i == strike ? strike.to_i : strike
|
|
37
|
+
sym = "#{stock.ticker}_#{expires_on.strftime("%m%d%y")}#{p_c_}#{strike_}" # XYZ_011819P45
|
|
38
|
+
self[:symbol] = sym
|
|
39
|
+
save
|
|
40
|
+
end
|
|
41
|
+
self[:symbol]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
##
|
|
45
|
+
## black-scholes pricing
|
|
46
|
+
##
|
|
47
|
+
|
|
48
|
+
=begin
|
|
49
|
+
##
|
|
50
|
+
##
|
|
51
|
+
##
|
|
52
|
+
annual to daily:
|
|
53
|
+
|
|
54
|
+
AR = ((DR + 1)^365 – 1) x 100
|
|
55
|
+
|
|
56
|
+
##
|
|
57
|
+
##
|
|
58
|
+
##
|
|
59
|
+
From: https://www.investopedia.com/articles/optioninvestor/07/options_beat_market.asp
|
|
60
|
+
|
|
61
|
+
K :: strike price
|
|
62
|
+
S_t :: last
|
|
63
|
+
r :: risk-free rate
|
|
64
|
+
t :: time to maturity
|
|
65
|
+
|
|
66
|
+
C = S_t N( d1 ) - K e^-rt N( d2 )
|
|
67
|
+
|
|
68
|
+
d1 = ln( St / K ) + (r + theta**2 / 2 )t
|
|
69
|
+
/{ theta_s * sqrt( t ) }
|
|
70
|
+
|
|
71
|
+
d2 = d1 - theta_s sqrt( t )
|
|
72
|
+
|
|
73
|
+
##
|
|
74
|
+
## From: https://en.wikipedia.org/wiki/Black%E2%80%93Scholes_model
|
|
75
|
+
##
|
|
76
|
+
|
|
77
|
+
D :: e^(rt) # discount factor
|
|
78
|
+
F :: e^(rt) S # forward price of underlying
|
|
79
|
+
|
|
80
|
+
C(F,t) = D[ N(d1)F - N(d2)K ]
|
|
81
|
+
|
|
82
|
+
d1 = ln(F/K) + stdev**2 t / 2
|
|
83
|
+
/{ stdev sqrt(t) }
|
|
84
|
+
d2 = d1 - stdev sqrt(t)
|
|
85
|
+
|
|
86
|
+
##
|
|
87
|
+
## From: https://www.daytrading.com/options-pricing-models
|
|
88
|
+
##
|
|
89
|
+
C0 = S0N(d1) – Xe-rtN(d2)
|
|
90
|
+
|
|
91
|
+
C0 = current call premium
|
|
92
|
+
S0 = current stock price
|
|
93
|
+
N(d1) = the probability that a value in a normal distribution will be less than d
|
|
94
|
+
N(d2) = the probability that the option will be in the money by expiration
|
|
95
|
+
X = strike price of the option
|
|
96
|
+
T = time until expiration (expressed in years)
|
|
97
|
+
r = risk-free interest rate
|
|
98
|
+
e = 2.71828, the base of the natural logarithm
|
|
99
|
+
ln = natural logarithm function
|
|
100
|
+
σ = standard deviation of the stock’s annualized rate of return (compounded continuously)
|
|
101
|
+
d1 = ln(S0/X) + (r + σ2/2)Tσ√T
|
|
102
|
+
|
|
103
|
+
d2 = d1 – σ√T
|
|
104
|
+
|
|
105
|
+
Note that:
|
|
106
|
+
|
|
107
|
+
Xe-rt = X/ert = the present value of the strike price using a continuously compounded interest rate
|
|
108
|
+
|
|
109
|
+
##
|
|
110
|
+
## From: https://www.wallstreetmojo.com/black-scholes-model/
|
|
111
|
+
##
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
## init
|
|
115
|
+
require 'distribution'
|
|
116
|
+
N = Distribution::Normal
|
|
117
|
+
stock = Iro::Stock.find_by ticker: 'NVDA'
|
|
118
|
+
strike = 910.0
|
|
119
|
+
r = Iro::Option.rate_daily
|
|
120
|
+
stdev = 91.0
|
|
121
|
+
t = 7.0
|
|
122
|
+
expires_on = '2024-03-22'
|
|
123
|
+
|
|
124
|
+
=end
|
|
125
|
+
def d1
|
|
126
|
+
last = stock.last
|
|
127
|
+
r = self.class.rate_annual
|
|
128
|
+
|
|
129
|
+
out = Math.log( last / strike ) + ( r + stdev**2 / 2 ) * t
|
|
130
|
+
out = out /( stdev * Math.sqrt(t) )
|
|
131
|
+
return out
|
|
132
|
+
end
|
|
133
|
+
def d2
|
|
134
|
+
last = stock.last
|
|
135
|
+
r = self.class.rate_annual
|
|
136
|
+
|
|
137
|
+
out = d1 - stdev * Math.sqrt( t )
|
|
138
|
+
return out
|
|
139
|
+
end
|
|
140
|
+
def t
|
|
141
|
+
# t = 1.0 / 365 * Date.today.business_days_until( expires_on )
|
|
142
|
+
t = 1.0 / 365 * (expires_on - Date.today).to_i
|
|
143
|
+
end
|
|
144
|
+
def stdev
|
|
145
|
+
recompute = nil
|
|
146
|
+
stock.stdev( recompute: recompute )
|
|
147
|
+
end
|
|
148
|
+
def call_price
|
|
149
|
+
last = stock.last
|
|
150
|
+
r = self.class.rate_annual
|
|
151
|
+
|
|
152
|
+
out = N.cdf( d1 ) * last - N.cdf( d2 ) * strike * Math::E**( -1 * r * t )
|
|
153
|
+
return out
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def put_price
|
|
157
|
+
last = stock.last
|
|
158
|
+
r = self.class.rate_annual
|
|
159
|
+
|
|
160
|
+
out = N.cdf(-d2) * strike * exp(-r*t) - N.cdf(-d1) * last
|
|
161
|
+
return out
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def self.rate_annual
|
|
166
|
+
0.05
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
=begin
|
|
170
|
+
# test
|
|
171
|
+
|
|
172
|
+
inn = 100
|
|
173
|
+
n.times { inn = inn*(1.0+out) }
|
|
174
|
+
inn
|
|
175
|
+
|
|
176
|
+
=end
|
|
177
|
+
def self.rate_daily
|
|
178
|
+
n = 250.0 # days
|
|
179
|
+
# n = 12 # months
|
|
180
|
+
|
|
181
|
+
out = (1.0+self.rate_annual)**(1.0/n) - 1.0
|
|
182
|
+
puts! out, 'rate_daily'
|
|
183
|
+
return out
|
|
184
|
+
end
|
|
15
185
|
|
|
16
186
|
end
|
data/app/models/iro/purse.rb
CHANGED
|
@@ -14,7 +14,10 @@ class Iro::Purse
|
|
|
14
14
|
belongs_to :stock, class_name: 'Iro::Stock', inverse_of: :strategies
|
|
15
15
|
|
|
16
16
|
field :unit, type: :integer, default: 10
|
|
17
|
-
|
|
17
|
+
|
|
18
|
+
## for rolling only:
|
|
19
|
+
field :height, type: :integer, default: 100
|
|
20
|
+
|
|
18
21
|
field :mark_every_n_usd, type: :float, default: 1
|
|
19
22
|
field :n_next_positions, type: :integer, default: 5
|
|
20
23
|
## with unit 10, sum_scale .001
|
data/app/models/iro/stock.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
include Math
|
|
2
|
+
require 'business_time'
|
|
1
3
|
|
|
2
4
|
class Iro::Stock
|
|
3
5
|
include Mongoid::Document
|
|
@@ -20,9 +22,16 @@ class Iro::Stock
|
|
|
20
22
|
field :last, type: :float
|
|
21
23
|
field :options_price_increment, type: :float
|
|
22
24
|
|
|
25
|
+
field :stdev, type: :float
|
|
26
|
+
|
|
23
27
|
has_many :positions, class_name: 'Iro::Position', inverse_of: :stock
|
|
24
28
|
has_many :strategies, class_name: 'Iro::Strategy', inverse_of: :stock
|
|
25
29
|
has_many :purses, class_name: 'Iro::Purse', inverse_of: :stock
|
|
30
|
+
has_many :options, class_name: 'Iro::Option', inverse_of: :stock
|
|
31
|
+
|
|
32
|
+
def self.f ticker
|
|
33
|
+
self.find_by ticker: ticker
|
|
34
|
+
end
|
|
26
35
|
|
|
27
36
|
def to_s
|
|
28
37
|
ticker
|
|
@@ -33,4 +42,76 @@ class Iro::Stock
|
|
|
33
42
|
def self.tickers_list
|
|
34
43
|
[[nil,nil]] + all.map { |sss| [ sss.ticker, sss.ticker ] }
|
|
35
44
|
end
|
|
45
|
+
|
|
46
|
+
=begin
|
|
47
|
+
stock = Iro::Stock.find_by( ticker: 'NVDA' )
|
|
48
|
+
|
|
49
|
+
duration = 1.month
|
|
50
|
+
stock.volatility_from_mo
|
|
51
|
+
|
|
52
|
+
duration = 1.year
|
|
53
|
+
stock.volatility_from_yr
|
|
54
|
+
|
|
55
|
+
=end
|
|
56
|
+
def volatility duration:
|
|
57
|
+
stock = self
|
|
58
|
+
begin_on = Time.now - duration - 1.day
|
|
59
|
+
points = Iro::Datapoint.where( kind: 'STOCK', symbol: stock.ticker,
|
|
60
|
+
:date.gte => begin_on,
|
|
61
|
+
).order_by( date: :asc )
|
|
62
|
+
|
|
63
|
+
puts! [points.first.date, points.last.date], "from,to"
|
|
64
|
+
|
|
65
|
+
points_p = []
|
|
66
|
+
points.each_with_index do |p, idx|
|
|
67
|
+
next if idx == 0
|
|
68
|
+
prev = points[idx-1]
|
|
69
|
+
|
|
70
|
+
out = p.value / prev.value - 1
|
|
71
|
+
points_p.push out
|
|
72
|
+
end
|
|
73
|
+
n = points_p.length
|
|
74
|
+
|
|
75
|
+
avg = points_p.reduce(&:+) / n
|
|
76
|
+
_sum_of_sq = []
|
|
77
|
+
points_p.map do |p|
|
|
78
|
+
_sum_of_sq.push( ( p - avg )*( p - avg ) )
|
|
79
|
+
end
|
|
80
|
+
sum_of_sq = _sum_of_sq.reduce( &:+ ) / n
|
|
81
|
+
|
|
82
|
+
# n_periods = begin_on.to_date.business_days_until( Date.today )
|
|
83
|
+
out = Math.sqrt( sum_of_sq )*sqrt( n )
|
|
84
|
+
adjustment = 2.0
|
|
85
|
+
out = out * adjustment
|
|
86
|
+
puts! out, 'volatility (adjusted)'
|
|
87
|
+
return out
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def volatility_from_mo
|
|
91
|
+
volatility( duration: 1.month )
|
|
92
|
+
end
|
|
93
|
+
def volatility_from_yr
|
|
94
|
+
volatility( duration: 1.year )
|
|
95
|
+
end
|
|
96
|
+
def stdev recompute: nil
|
|
97
|
+
if !self[:stdev] || recompute
|
|
98
|
+
out = volatility_from_yr
|
|
99
|
+
self[:stdev] = out
|
|
100
|
+
save( validate: false )
|
|
101
|
+
return out
|
|
102
|
+
else
|
|
103
|
+
self[:stdev]
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
## stdev
|
|
108
|
+
## From: https://stackoverflow.com/questions/19484891/how-do-i-find-the-standard-deviation-in-ruby
|
|
109
|
+
# contents = [1,2,3,4,5,6,7,8,9]
|
|
110
|
+
# n = contents.size # => 9
|
|
111
|
+
# contents.map!(&:to_f) # => [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]
|
|
112
|
+
# mean = contents.reduce(&:+)/n # => 5.0
|
|
113
|
+
# sum_sqr = contents.map {|x| x * x}.reduce(&:+) # => 285.0
|
|
114
|
+
# std_dev = Math.sqrt((sum_sqr - n * mean * mean)/(n-1)) # => 2.7386127875258306
|
|
115
|
+
|
|
116
|
+
|
|
36
117
|
end
|
data/app/models/tda/option.rb
CHANGED
|
@@ -125,13 +125,109 @@ class Tda::Option
|
|
|
125
125
|
end
|
|
126
126
|
|
|
127
127
|
|
|
128
|
+
def self.close_credit_call
|
|
129
|
+
end
|
|
130
|
+
def self.close_long_debit_call_spread
|
|
131
|
+
end
|
|
132
|
+
def self.close_short_debit_put_spread
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def self.get_token
|
|
136
|
+
opts = {
|
|
137
|
+
grant_type: 'authorization_code',
|
|
138
|
+
access_type: 'offline',
|
|
139
|
+
code: ::TD_AMERITRADE[:code],
|
|
140
|
+
}
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def self.create_credit_call outer:, inner:, q:, price:
|
|
144
|
+
query = {
|
|
145
|
+
orderType: "NET_DEBIT",
|
|
146
|
+
session: "NORMAL",
|
|
147
|
+
price: price,
|
|
148
|
+
duration: "DAY",
|
|
149
|
+
orderStrategyType: "SINGLE",
|
|
150
|
+
orderLegCollection: [
|
|
151
|
+
{
|
|
152
|
+
instruction: "BUY_TO_OPEN",
|
|
153
|
+
quantity: q,
|
|
154
|
+
instrument: {
|
|
155
|
+
symbol: outer.symbol,
|
|
156
|
+
assetType: "OPTION",
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
instruction: "SELL_TO_OPEN",
|
|
161
|
+
quantity: q,
|
|
162
|
+
instrument: {
|
|
163
|
+
symbol: inner.symbol,
|
|
164
|
+
assetType: "OPTION",
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
}
|
|
169
|
+
File.write('tmp/query.json', JSON.pretty_generate( query ))
|
|
170
|
+
puts! query, 'query'
|
|
171
|
+
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
headers = {
|
|
175
|
+
Authorize: "Bearer #{::TD_AMERITRADE[:access_token]}",
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
path = "/v1/accounts/#{::TD_AMERITRADE[:accountId]}/orders"
|
|
179
|
+
puts! path, 'path'
|
|
180
|
+
out = self.post path, { query: query, headers: headers }
|
|
181
|
+
timestamp = DateTime.parse out.headers['date']
|
|
182
|
+
out = out.parsed_response.deep_symbolize_keys
|
|
183
|
+
puts! out, 'created credit call?'
|
|
184
|
+
end
|
|
185
|
+
def self.create_long_debit_call_spread
|
|
186
|
+
end
|
|
187
|
+
def self.create_short_debit_put_spread
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def self.roll_credit_call
|
|
191
|
+
end
|
|
192
|
+
def self.roll_long_debit_call_spread
|
|
193
|
+
end
|
|
194
|
+
def self.roll_short_debit_put_spread
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
|
|
128
198
|
end
|
|
129
199
|
|
|
200
|
+
##
|
|
201
|
+
## From: https://developer.tdameritrade.com/content/place-order-samples
|
|
202
|
+
## Buy Limit: Vertical Call Spread
|
|
203
|
+
##
|
|
130
204
|
=begin
|
|
205
|
+
{
|
|
206
|
+
"orderType": "NET_DEBIT",
|
|
207
|
+
"session": "NORMAL",
|
|
208
|
+
"price": "1.20",
|
|
209
|
+
"duration": "DAY",
|
|
210
|
+
"orderStrategyType": "SINGLE",
|
|
211
|
+
"orderLegCollection": [
|
|
212
|
+
{
|
|
213
|
+
"instruction": "BUY_TO_OPEN",
|
|
214
|
+
"quantity": 10,
|
|
215
|
+
"instrument": {
|
|
216
|
+
"symbol": "XYZ_011516C40",
|
|
217
|
+
"assetType": "OPTION"
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
"instruction": "SELL_TO_OPEN",
|
|
222
|
+
"quantity": 10,
|
|
223
|
+
"instrument": {
|
|
224
|
+
"symbol": "XYZ_011516C42.5",
|
|
225
|
+
"assetType": "OPTION"
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
}
|
|
230
|
+
=end
|
|
231
|
+
|
|
131
232
|
|
|
132
|
-
outs = Tda::Option.get_quotes({
|
|
133
|
-
contractType: 'CALL', strike: 20.0, expirationDate: '2024-01-12',
|
|
134
|
-
ticker: 'GME',
|
|
135
|
-
})
|
|
136
233
|
|
|
137
|
-
=end
|
data/lib/tasks/db_tasks.rake
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
namespace :db do
|
|
3
3
|
|
|
4
|
-
##
|
|
4
|
+
## Date, Volume, Open, High, Low, Close
|
|
5
5
|
desc 'import_stock symbol=GME path=./data/GME-test.csv'
|
|
6
6
|
task import_stock: :environment do
|
|
7
7
|
Iro::Datapoint.import_stock( symbol: ENV['symbol'], path: ENV['path'] )
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
|
|
2
|
+
namespace :test do
|
|
3
|
+
|
|
4
|
+
desc 'stock#volatility_mo'
|
|
5
|
+
task stock_vol_mo: :environment do
|
|
6
|
+
out = Iro::Stock.find_by( ticker: 'NVDA' ).volatility_from_mo
|
|
7
|
+
puts! out, 'out'
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
desc 'stock#volatility_yr'
|
|
11
|
+
task stock_vol_yr: :environment do
|
|
12
|
+
out = Iro::Stock.find_by( ticker: 'NVDA' ).volatility_from_yr
|
|
13
|
+
puts! out, 'out'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
desc 'option#call_price'
|
|
17
|
+
task option_call_price: :environment do
|
|
18
|
+
stock = Iro::Stock.find_by( ticker: 'NVDA' )
|
|
19
|
+
stock.last = 884
|
|
20
|
+
|
|
21
|
+
option = Iro::Option.new({
|
|
22
|
+
stock: stock,
|
|
23
|
+
strike: 910,
|
|
24
|
+
expires_on: '2024-03-22',
|
|
25
|
+
# delta: 0.41,
|
|
26
|
+
})
|
|
27
|
+
out = option.put_price
|
|
28
|
+
puts! out, 'out'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
desc 'place order'
|
|
32
|
+
task place_order: :environment do
|
|
33
|
+
stock = Iro::Stock.f 'GME'
|
|
34
|
+
outer = Iro::Option.new({
|
|
35
|
+
stock: stock,
|
|
36
|
+
put_call: 'CALL',
|
|
37
|
+
strike: 15.5,
|
|
38
|
+
expires_on: '2024-03-22',
|
|
39
|
+
})
|
|
40
|
+
inner = Iro::Option.new({
|
|
41
|
+
stock: stock,
|
|
42
|
+
put_call: 'CALL',
|
|
43
|
+
strike: 17.0,
|
|
44
|
+
expires_on: '2024-03-22',
|
|
45
|
+
})
|
|
46
|
+
out = Tda::Option.create_credit_call( inner: inner, outer: outer, q: 1, price: 0.01 )
|
|
47
|
+
puts! out, 'out'
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: iron_warbler
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.7.
|
|
4
|
+
version: 2.0.7.24
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Victor Pudeyev
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2024-03-
|
|
11
|
+
date: 2024-03-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: business_time
|
|
@@ -192,12 +192,27 @@ dependencies:
|
|
|
192
192
|
- - "~>"
|
|
193
193
|
- !ruby/object:Gem::Version
|
|
194
194
|
version: 1.0.1
|
|
195
|
+
- !ruby/object:Gem::Dependency
|
|
196
|
+
name: distribution
|
|
197
|
+
requirement: !ruby/object:Gem::Requirement
|
|
198
|
+
requirements:
|
|
199
|
+
- - ">="
|
|
200
|
+
- !ruby/object:Gem::Version
|
|
201
|
+
version: '0'
|
|
202
|
+
type: :runtime
|
|
203
|
+
prerelease: false
|
|
204
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
205
|
+
requirements:
|
|
206
|
+
- - ">="
|
|
207
|
+
- !ruby/object:Gem::Version
|
|
208
|
+
version: '0'
|
|
195
209
|
description: A stocks and Options Trading Bot.
|
|
196
210
|
email: victor@wasya.co
|
|
197
211
|
executables: []
|
|
198
212
|
extensions: []
|
|
199
213
|
extra_rdoc_files: []
|
|
200
214
|
files:
|
|
215
|
+
- README.txt
|
|
201
216
|
- Rakefile
|
|
202
217
|
- app/assets/config/iron_warbler_manifest.js
|
|
203
218
|
- app/assets/javascript/iron_warbler/application.js
|
|
@@ -232,8 +247,6 @@ files:
|
|
|
232
247
|
- app/models/iro/purse.rb
|
|
233
248
|
- app/models/iro/stock.rb
|
|
234
249
|
- app/models/iro/strategy.rb
|
|
235
|
-
- app/models/iro/trash/position_covered_call.rb
|
|
236
|
-
- app/models/iro/trash/position_debit_spread.rb
|
|
237
250
|
- app/models/tda/option.rb
|
|
238
251
|
- app/models/tda/stock.rb
|
|
239
252
|
- app/views/iro/_analytics.erb
|
|
@@ -293,6 +306,7 @@ files:
|
|
|
293
306
|
- lib/tasks/db_tasks.rake
|
|
294
307
|
- lib/tasks/iro_tasks.rake
|
|
295
308
|
- lib/tasks/iro_tasks.rake-old
|
|
309
|
+
- lib/tasks/test_tasks.rake
|
|
296
310
|
homepage: https://wasya.co
|
|
297
311
|
licenses:
|
|
298
312
|
- Proprietary
|
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
class Iro::PositionDebitSpread < Iro::Positin
|
|
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
|
-
index({ purse_id: 1, ticker: 1 })
|
|
16
|
-
|
|
17
|
-
belongs_to :stock, class_name: 'Iro::Stock', inverse_of: :positions
|
|
18
|
-
def ticker
|
|
19
|
-
stock&.ticker || '-'
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
belongs_to :strategy, class_name: 'Iro::Strategy', inverse_of: :positions
|
|
23
|
-
|
|
24
|
-
field :outer_strike, type: :float
|
|
25
|
-
validates :outer_strike, presence: true
|
|
26
|
-
|
|
27
|
-
field :inner_strike, type: :float
|
|
28
|
-
validates :inner_strike, presence: true
|
|
29
|
-
|
|
30
|
-
field :expires_on
|
|
31
|
-
validates :expires_on, presence: true
|
|
32
|
-
|
|
33
|
-
field :quantity, type: :integer
|
|
34
|
-
validates :quantity, presence: true
|
|
35
|
-
|
|
36
|
-
field :begin_on
|
|
37
|
-
field :begin_outer_price, type: :float
|
|
38
|
-
field :begin_outer_delta, type: :float
|
|
39
|
-
|
|
40
|
-
field :begin_inner_price, type: :float
|
|
41
|
-
field :begin_inner_delta, type: :float
|
|
42
|
-
|
|
43
|
-
field :end_on
|
|
44
|
-
field :end_outer_price, type: :float
|
|
45
|
-
field :end_outer_delta, type: :float
|
|
46
|
-
|
|
47
|
-
field :end_inner_price, type: :float
|
|
48
|
-
field :end_inner_delta, type: :float
|
|
49
|
-
|
|
50
|
-
field :net_amount
|
|
51
|
-
field :net_percent
|
|
52
|
-
|
|
53
|
-
def current_underlying_strike
|
|
54
|
-
Iro::Stock.find_by( ticker: ticker ).last
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def refresh
|
|
58
|
-
out = Tda::Option.get_quote({
|
|
59
|
-
contractType: 'CALL',
|
|
60
|
-
strike: strike,
|
|
61
|
-
expirationDate: expires_on,
|
|
62
|
-
ticker: ticker,
|
|
63
|
-
})
|
|
64
|
-
update({
|
|
65
|
-
end_delta: out[:delta],
|
|
66
|
-
end_price: out[:last],
|
|
67
|
-
})
|
|
68
|
-
print '_'
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def net_amount # total
|
|
72
|
-
outer = 0 - begin_outer_price + end_outer_price
|
|
73
|
-
inner = begin_inner_price - end_inner_price
|
|
74
|
-
out = ( outer + inner ) * 100 * quantity
|
|
75
|
-
end
|
|
76
|
-
def max_gain # total
|
|
77
|
-
100 * ( begin_outer_price - begin_inner_price ) * quantity
|
|
78
|
-
end
|
|
79
|
-
def max_loss
|
|
80
|
-
out = 100 * ( outer_strike - inner_strike ) * quantity
|
|
81
|
-
if strategy.long_or_short == Iro::Strategy::SHORT
|
|
82
|
-
out = out * -1
|
|
83
|
-
end
|
|
84
|
-
return out
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
field :next_delta, type: :float
|
|
89
|
-
field :next_outcome, type: :float
|
|
90
|
-
field :next_symbol
|
|
91
|
-
field :next_mark
|
|
92
|
-
field :next_reasons, type: :array, default: []
|
|
93
|
-
field :should_rollp, type: :float
|
|
94
|
-
|
|
95
|
-
##
|
|
96
|
-
## decisions
|
|
97
|
-
##
|
|
98
|
-
|
|
99
|
-
def should_roll?
|
|
100
|
-
puts! 'shold_roll?'
|
|
101
|
-
|
|
102
|
-
update({
|
|
103
|
-
next_reasons: [],
|
|
104
|
-
next_symbol: nil,
|
|
105
|
-
next_delta: nil,
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
if must_roll?
|
|
109
|
-
out = 1.0
|
|
110
|
-
elsif can_roll?
|
|
111
|
-
|
|
112
|
-
if end_delta < strategy.threshold_delta
|
|
113
|
-
next_reasons.push "delta is lower than threshold"
|
|
114
|
-
out = 0.91
|
|
115
|
-
elsif 1 - end_outer_price/begin_outer_price > strategy.threshold_netp
|
|
116
|
-
next_reasons.push "made enough percent profit (dubious)"
|
|
117
|
-
out = 0.61
|
|
118
|
-
else
|
|
119
|
-
next_reasons.push "neutral"
|
|
120
|
-
out = 0.33
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
else
|
|
124
|
-
out = 0.0
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
update({
|
|
128
|
-
next_delta: next_position[:delta],
|
|
129
|
-
next_outcome: next_position[:mark] - end_price,
|
|
130
|
-
next_symbol: next_position[:symbol],
|
|
131
|
-
next_mark: next_position[:mark],
|
|
132
|
-
should_rollp: out,
|
|
133
|
-
# status: Iro::Position::STATE_PROPOSED,
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
puts! next_reasons, 'next_reasons'
|
|
137
|
-
puts! out, 'out'
|
|
138
|
-
return out > 0.5
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
## expires_on = cc.expires_on ; nil
|
|
143
|
-
def can_roll?
|
|
144
|
-
## only if less than 7 days left
|
|
145
|
-
( expires_on.to_date - Time.now.to_date ).to_i < 7
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
## If I'm near below water
|
|
149
|
-
##
|
|
150
|
-
## expires_on = cc.expires_on ; strategy = cc.strategy ; strike = cc.strike ; nil
|
|
151
|
-
def must_roll?
|
|
152
|
-
if ( current_underlying_strike + strategy.buffer_above_water ) > strike
|
|
153
|
-
return true
|
|
154
|
-
end
|
|
155
|
-
## @TODO: This one should not happen, I should log appropriately. _vp_ 2023-03-19
|
|
156
|
-
if ( expires_on.to_date - Time.now.to_date ).to_i < 1
|
|
157
|
-
return true
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
## strike = cc.strike ; strategy = cc.strategy ; nil
|
|
162
|
-
def near_below_water?
|
|
163
|
-
strike < current_underlying_strike + strategy.buffer_above_water
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
## 2023-03-18 _vp_ Continue.
|
|
169
|
-
## 2023-03-19 _vp_ Continue.
|
|
170
|
-
## 2023-08-05 _vp_ an Important method
|
|
171
|
-
##
|
|
172
|
-
## expires_on = cc.expires_on ; strategy = cc.strategy ; ticker = cc.ticker ; end_price = cc.end_price ; next_expires_on = cc.next_expires_on ; nil
|
|
173
|
-
##
|
|
174
|
-
## out.map { |p| [ p[:strikePrice], p[:delta] ] }
|
|
175
|
-
##
|
|
176
|
-
def next_position
|
|
177
|
-
return @next_position if @next_position
|
|
178
|
-
return {} if ![ STATUS_ACTIVE, STATUS_PROPOSED ].include?( status )
|
|
179
|
-
|
|
180
|
-
## 7 days ahead - not configurable so far
|
|
181
|
-
out = Tda::Option.get_quotes({
|
|
182
|
-
ticker: ticker,
|
|
183
|
-
expirationDate: next_expires_on,
|
|
184
|
-
contractType: 'CALL',
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
## above_water
|
|
188
|
-
if strategy.buffer_above_water.present?
|
|
189
|
-
out = out.select do |i|
|
|
190
|
-
i[:strikePrice] > current_underlying_strike + strategy.buffer_above_water
|
|
191
|
-
end
|
|
192
|
-
# next_reasons.push "buffer_above_water above #{current_underlying_strike + strategy.buffer_above_water}"
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
if near_below_water?
|
|
196
|
-
msg = "Panic! climb at a loss. Skip the rest of the calculation."
|
|
197
|
-
next_reasons.push msg
|
|
198
|
-
## @TODO: if not enough money in the purse, cannot roll? 2023-03-19
|
|
199
|
-
|
|
200
|
-
# byebug
|
|
201
|
-
|
|
202
|
-
## Take a small loss here.
|
|
203
|
-
prev = nil
|
|
204
|
-
out.each_with_index do |i, idx|
|
|
205
|
-
next if idx == 0
|
|
206
|
-
if i[:last] < end_price
|
|
207
|
-
prev ||= i
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
out = [ prev ]
|
|
211
|
-
|
|
212
|
-
else
|
|
213
|
-
## Normal flow, making money.
|
|
214
|
-
## @TODO: test! _vp_ 2023-03-19
|
|
215
|
-
|
|
216
|
-
## next_min_strike
|
|
217
|
-
if strategy.next_min_strike.present?
|
|
218
|
-
out = out.select do |i|
|
|
219
|
-
i[:strikePrice] >= strategy.next_min_strike
|
|
220
|
-
end
|
|
221
|
-
# next_reasons.push "next_min_strike above #{strategy.next_min_strike}"
|
|
222
|
-
end
|
|
223
|
-
# json_puts! out.map { |p| [p[:delta], p[:symbol]] }, 'next_min_strike'
|
|
224
|
-
|
|
225
|
-
## max_delta
|
|
226
|
-
if strategy.next_max_delta.present?
|
|
227
|
-
out = out.select do |i|
|
|
228
|
-
i[:delta] = 0.0 if i[:delta] == "NaN"
|
|
229
|
-
i[:delta] <= strategy.next_max_delta
|
|
230
|
-
end
|
|
231
|
-
# next_reasons.push "next_max_delta below #{strategy.next_max_delta}"
|
|
232
|
-
end
|
|
233
|
-
# json_puts! out.map { |p| [p[:delta], p[:symbol]] }, 'next_max_delta'
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
@next_position = out[0] || {}
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
## @TODO: Test this. _vp_ 2023-04-01
|
|
240
|
-
def next_expires_on
|
|
241
|
-
out = expires_on.to_time + 7.days
|
|
242
|
-
while !out.friday?
|
|
243
|
-
out = out + 1.day
|
|
244
|
-
end
|
|
245
|
-
while !out.workday?
|
|
246
|
-
out = out - 1.day
|
|
247
|
-
end
|
|
248
|
-
return out
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
end
|