wco_models 3.1.0.184 → 3.1.0.188

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8bf5a9dd6c6124e378294fc3b5ff4f7fbc5f04eb7b26142eb5bf18b6b9a1898
4
- data.tar.gz: f1a168331f27470048fa2508acc8ba2c179bea02a8c19ff48865e77d551b2fbf
3
+ metadata.gz: 7f8e0b2d84f450124f19512789d6ab1c3811497c536e05486d410b25a9d9e77a
4
+ data.tar.gz: d88b39a9d47c902bfac62d8f97f34e7cb12ca15f7e3730ebf166b73856a18806
5
5
  SHA512:
6
- metadata.gz: c4e4b0ce772fcb3526197b7ebf71b2b175c0e32d4ecfc8679e59d73cb5f24591531b25f80f6bd3472ed3d3c48ba2376c90315b2aa44a01ffc7f9ebf58fb293d2
7
- data.tar.gz: cfcd418cafaeba04479f597267a174631e94208cdff3387f5cb991c50af2be88867dfc27b65be5f0a2e8be8d49ffc843631ccf4a37c1f5aedc538b3f6960171b
6
+ metadata.gz: a2bbcc52eb28b820913e885171a0006d2f28d123144ae51dbf6092b84bf54640b03255a731715c9e29664b6b17ea036b143b87e691bf28053c63191c39b2aca9
7
+ data.tar.gz: e39abef4f5a2b7d3eaf494312737267b02b5515ad13942d9b0c81462a32af03a5a640c577cdadd0a1e80a32d62bd98811d1da19eba7f32e8a5c8ed36d0adac9b
data/README.txt CHANGED
@@ -1,2 +1,11 @@
1
1
 
2
- IshModels, soon to be WcoModels
2
+ Wco Models.
3
+
4
+ == Test ==
5
+
6
+ Login to the localstack container, then:
7
+
8
+ awslocal s3api put-object --bucket wco-email-ses-development \
9
+ --key 00nn652jk1395ujdr3l11ib06jam0oevjqv2o4g1 \
10
+ --body /opt/tmp/00nn652jk1395ujdr3l11ib06jam0oevjqv2o4g1
11
+
@@ -0,0 +1,63 @@
1
+
2
+ class Iro::Alert
3
+ include Mongoid::Document
4
+ include Mongoid::Timestamps
5
+ include Mongoid::Paranoia
6
+ store_in collection: 'iro_alerts'
7
+
8
+ # SLEEP_TIME_SECONDS = Rails.env.production? ? 60 : 15
9
+
10
+ DIRECTION_ABOVE = 'ABOVE'
11
+ DIRECTION_BELOW = 'BELOW'
12
+ def self.directions_list
13
+ [ nil, DIRECTION_ABOVE, DIRECTION_BELOW ]
14
+ end
15
+
16
+ STATUS_ACTIVE = 'active'
17
+ STATUS_INACTIVE = 'inactive'
18
+ STATUSES = [ nil, 'active', 'inactive' ]
19
+ field :status, default: STATUS_ACTIVE
20
+ def self.active
21
+ where( status: STATUS_ACTIVE )
22
+ end
23
+
24
+ field :class_name, default: 'Iro::Stock'
25
+ validates :class_name, presence: true
26
+
27
+ field :symbol, type: String
28
+ validates :symbol, presence: true
29
+
30
+ field :direction, type: String
31
+ validates :direction, presence: true
32
+
33
+ field :strike, type: Float
34
+ validates :strike, presence: true
35
+
36
+ def do_run
37
+ alert = self
38
+ begin
39
+ price = Tda::Stock.get_quote( alert.symbol )&.last
40
+ return if !price
41
+
42
+ if ( alert.direction == alert.class::DIRECTION_ABOVE && price >= alert.strike ) ||
43
+ ( alert.direction == alert.class::DIRECTION_BELOW && price <= alert.strike )
44
+
45
+ if Rails.env.production?
46
+ Iro::AlertMailer.stock_alert( alert.id.to_s ).deliver_later
47
+ else
48
+ Iro::AlertMailer.stock_alert( alert.id.to_s ).deliver_now
49
+ end
50
+ alert.update({ status: alert.class::STATUS_INACTIVE })
51
+ print '^'
52
+
53
+ end
54
+ rescue => err
55
+ puts! err, 'err'
56
+ ::ExceptionNotifier.notify_exception(
57
+ err,
58
+ data: { alert: alert }
59
+ )
60
+ end
61
+ end
62
+
63
+ end
@@ -0,0 +1,187 @@
1
+
2
+ ##
3
+ ## Datapoints are at most daily!
4
+ ## See Priceitem for intra-day data
5
+ ##
6
+ class Iro::Datapoint
7
+ include Mongoid::Document
8
+ include Mongoid::Timestamps
9
+ store_in collection: 'iro_datapoints'
10
+
11
+ field :kind
12
+ validates :kind, presence: true
13
+ index({ kind: -1 })
14
+ KIND_CRYPTO = 'CRYPTO'
15
+ KIND_STOCK = 'STOCK'
16
+ KIND_OPTION = 'OPTION' ## but not PUT or CALL
17
+ KIND_CURRENCY = 'CURRENCY'
18
+ KIND_TREASURY = 'TREASURY'
19
+
20
+ field :symbol ## ticker, but use 'symbol' here
21
+ ## crypto
22
+ SYMBOL_BTC = 'BTC'
23
+ SYMBOL_ETH = 'ETH'
24
+ ## currencies
25
+ SYMBOL_JPY = 'JPY'
26
+ SYMBOL_COP = 'COP'
27
+ SUMBOL_EUR = 'EUR'
28
+ ## treasuries
29
+ SYMBOL_T1MO = 'T1MO'
30
+ SYMBOL_T2MO = 'T2MO'
31
+ SYMBOL_T3MO = 'T3MO'
32
+ SYMBOL_T4MO = 'T4MO'
33
+ SYMBOL_T6MO = 'T6MO'
34
+ SYMBOL_T1YR = 'T1YR'
35
+ SYMBOL_T2YR = 'T2YR'
36
+ SYMBOL_T3YR = 'T3YR'
37
+ SYMBOL_T5YR = 'T5YR'
38
+ SYMBOL_T7YR = 'T7YR'
39
+ SYMBOL_T10YR = 'T10YR'
40
+ SYMBOL_T20YR = 'T20YR'
41
+ SYMBOL_T30YR = 'T30YR'
42
+
43
+ field :date, type: Date
44
+ index({ kind: -1, date: -1 })
45
+ validates :date, uniqueness: { scope: [ :symbol ] }
46
+
47
+ field :quote_at, type: DateTime
48
+ index({ kind: -1, quote_at: -1 })
49
+ validates :quote_at, uniqueness: { scope: [ :kind, :symbol ] } ## scope-by-kind is unnecessary here? _vp_ 2024-08-08
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
56
+
57
+ field :value, type: Float
58
+ validates :value, presence: true
59
+
60
+
61
+ field :volume, type: Integer
62
+
63
+
64
+ def self.test_0trash
65
+ add_fields = { '$addFields': {
66
+ 'date_string': {
67
+ '$dateToString': { 'format': "%Y-%m-%d", 'date': "$created_at" }
68
+ }
69
+ } }
70
+ # group = { '$group': {
71
+ # '_id': "$date_string",
72
+ # 'my_doc': { '$first': "$$ROOT" }
73
+ # } }
74
+ group = { '$group': {
75
+ '_id': "$date",
76
+ 'my_doc': { '$first': "$$ROOT" }
77
+ } }
78
+ lookup = { '$lookup': {
79
+ 'from': 'iro_dates',
80
+ 'localField': 'date_string',
81
+ 'foreignField': 'date',
82
+ 'as': 'dates',
83
+ } }
84
+ lookup_merge = { '$replaceRoot': {
85
+ 'newRoot': { '$mergeObjects': [
86
+ { '$arrayElemAt': [ "$dates", 0 ] }, "$$ROOT"
87
+ ] }
88
+ } }
89
+ match = { '$match': {
90
+ 'kind': 'some-type',
91
+ 'created_at': {
92
+ '$gte': '2023-12-01'.to_datetime,
93
+ '$lte': '2023-12-31'.to_datetime,
94
+ }
95
+ } }
96
+
97
+ outs = Iro::Datapoint.collection.aggregate([
98
+ add_fields,
99
+ lookup, lookup_merge,
100
+ match,
101
+
102
+ { '$sort': { 'date_string': 1 } },
103
+ group,
104
+ # { '$replaceRoot': { 'newRoot': "$my_doc" } },
105
+ # { '$project': { '_id': 0, 'date_string': 1, 'value': 1 } },
106
+ ])
107
+
108
+ puts! 'result'
109
+ pp outs.to_a
110
+ # puts! outs.to_a, 'result'
111
+ end
112
+
113
+ def self.test
114
+ lookup = { '$lookup': {
115
+ 'from': 'iro_datapoints',
116
+ 'localField': 'date',
117
+ 'foreignField': 'date',
118
+ 'pipeline': [
119
+ { '$sort': { 'value': -1 } },
120
+ ],
121
+ 'as': 'datapoints',
122
+ } }
123
+ lookup_merge = { '$replaceRoot': {
124
+ 'newRoot': { '$mergeObjects': [
125
+ { '$arrayElemAt': [ "$datapoints", 0 ] }, "$$ROOT"
126
+ ] }
127
+ } }
128
+
129
+
130
+ match = { '$match': {
131
+ 'date': {
132
+ '$gte': '2023-12-25',
133
+ '$lte': '2023-12-31',
134
+ }
135
+ } }
136
+
137
+ group = { '$group': {
138
+ '_id': "$date",
139
+ 'my_doc': { '$first': "$$ROOT" }
140
+ } }
141
+
142
+ outs = Iro::Date.collection.aggregate([
143
+ match,
144
+
145
+ lookup,
146
+ lookup_merge,
147
+
148
+ group,
149
+ { '$replaceRoot': { 'newRoot': "$my_doc" } },
150
+ # { '$replaceRoot': { 'newRoot': "$my_doc" } },
151
+
152
+
153
+ { '$project': { '_id': 0, 'date': 1, 'value': 1 } },
154
+ { '$sort': { 'date': 1 } },
155
+ ])
156
+
157
+ puts! 'result'
158
+ pp outs.to_a
159
+ # puts! outs.to_a, 'result'
160
+ end
161
+
162
+ def self.import_stock symbol:, path:
163
+ csv = CSV.read(path, headers: true)
164
+ csv.each do |row|
165
+ flag = create({
166
+ kind: KIND_STOCK,
167
+ symbol: symbol,
168
+ date: row['Date'],
169
+ quote_at: row['Date'],
170
+
171
+ volume: row['Volume'],
172
+
173
+ open: row['Open'],
174
+ high: row['High'],
175
+ low: row['Low'],
176
+ value: row['Close'],
177
+ })
178
+ if flag.persisted?
179
+ print '^'
180
+ else
181
+ puts flag.errors.messages
182
+ end
183
+ end
184
+ puts 'ok'
185
+ end
186
+
187
+ end
@@ -0,0 +1,10 @@
1
+
2
+ class Iro::Date
3
+ include Mongoid::Document
4
+ # include Mongoid::Timestamps
5
+ store_in collection: 'iro_dates'
6
+
7
+ field :date
8
+
9
+ end
10
+
@@ -0,0 +1,154 @@
1
+
2
+ class Iro::Option
3
+ include Mongoid::Document
4
+ include Mongoid::Timestamps
5
+ include Mongoid::Paranoia
6
+ include Iro::OptionBlackScholes
7
+ store_in collection: 'iro_options'
8
+
9
+ attr_accessor :recompute
10
+
11
+ belongs_to :stock, class_name: 'Iro::Stock', inverse_of: :strategies
12
+ def ticker; stock.ticker; end
13
+ # field :ticker
14
+ # validates :ticker, presence: true
15
+
16
+ CALL = 'CALL'
17
+ PUT = 'PUT'
18
+
19
+ field :symbol
20
+ ## each option can be a leg in a position, no uniqueness
21
+ # validates :symbol, uniqueness: true, presence: true
22
+
23
+ field :put_call, type: :string # 'PUT' or 'CALL'
24
+ validates :put_call, presence: true
25
+
26
+ field :delta, type: :float
27
+
28
+ field :strike, type: :float
29
+ validates :strike, presence: true
30
+
31
+ field :expires_on, type: :date
32
+ validates :expires_on, presence: true
33
+ def self.expirations_list full: false, n: 5
34
+ out = [[nil,nil]]
35
+ day = Date.today - 5.days
36
+ n.times do
37
+ next_exp = day.next_occurring(:thursday).next_occurring(:friday)
38
+ if !next_exp.workday?
39
+ next_exp = Time.previous_business_day( next_exp )
40
+ end
41
+
42
+ out.push([ next_exp.strftime('%b %e'), next_exp.strftime('%Y-%m-%d') ])
43
+ day = next_exp
44
+ end
45
+ return out
46
+ # [
47
+ # [ nil, nil ],
48
+ # [ 'Mar 22', '2024-03-22'.to_date ],
49
+ # [ 'Mar 28', '2024-03-28'.to_date ],
50
+ # [ 'Apr 5', '2024-04-05'.to_date ],
51
+ # [ 'Mar 12', '2024-03-12'.to_date ],
52
+ # [ 'Mar 19', '2024-03-19'.to_date ],
53
+ # ]
54
+ end
55
+
56
+ field :begin_price, type: :float
57
+ field :begin_delta, type: :float
58
+ field :end_price, type: :float
59
+ field :end_delta, type: :float
60
+
61
+
62
+ has_one :outer, class_name: 'Iro::Position', inverse_of: :outer
63
+ has_one :inner, class_name: 'Iro::Position', inverse_of: :inner
64
+
65
+ field :last, type: :float
66
+
67
+ ## for TDA
68
+ def symbol
69
+ if !self[:symbol]
70
+ p_c_ = put_call == 'PUT' ? 'P' : 'C'
71
+ strike_ = strike.to_i == strike ? strike.to_i : strike
72
+ sym = "#{stock.ticker}_#{expires_on.strftime("%m%d%y")}#{p_c_}#{strike_}" # XYZ_011819P45
73
+ self[:symbol] = sym
74
+ save
75
+ end
76
+ self[:symbol]
77
+ end
78
+
79
+ before_save :sync, if: ->() { !Rails.env.test? } ## do not sync in test
80
+ def sync
81
+ out = Tda::Option.get_quote({
82
+ contractType: put_call,
83
+ strike: strike,
84
+ expirationDate: expires_on,
85
+ ticker: ticker,
86
+ })
87
+ puts! out, 'option sync'
88
+ self.end_price = ( out.bid + out.ask ) / 2 rescue 0
89
+ self.end_delta = out.delta if out.delta
90
+ # self.save
91
+ end
92
+
93
+ def self.max_pain hash
94
+ outs = {}
95
+
96
+ %w| put call |.each do |contractType|
97
+ dates = hash["#{contractType}ExpDateMap"]
98
+ dates.each do |_date, strikes| ## _date="2023-02-10:5"
99
+ date = _date.split(':')[0].to_date.to_s
100
+ outs[date] ||= {
101
+ 'all' => {},
102
+ 'call' => {},
103
+ 'put' => {},
104
+ 'summary' => {},
105
+ }
106
+
107
+ strikes.each do |_strike, _v| ## _strike="18.5"
108
+ strike = _strike.to_f
109
+
110
+ ## calls
111
+ mem_c = 0
112
+ strikes.keys.reverse.each do |_key|
113
+ if _key == _strike
114
+ break
115
+ end
116
+ key = _key.to_f
117
+ tmp = hash["callExpDateMap"][_date][_key][0]['openInterest'] * ( key - strike )
118
+ mem_c += tmp
119
+ end
120
+ outs[date]['call'][_strike] = mem_c
121
+
122
+ ## puts
123
+ mem_p = 0
124
+ strikes.keys.each do |_key|
125
+ if _key == _strike
126
+ break
127
+ end
128
+ key = _key.to_f
129
+ tmp = hash["putExpDateMap"][_date][_key][0]['openInterest'] * ( strike - key )
130
+ mem_p += tmp
131
+ end
132
+ outs[date]['put'][_strike] = mem_p
133
+ outs[date]['all'][_strike] = mem_c + mem_p
134
+
135
+ end
136
+ end
137
+ end
138
+
139
+ ## compute summary
140
+ outs.each do |date, types|
141
+ all = types['all']
142
+ outs[date]['summary'] = { 'value' => all.keys[0] }
143
+ all.each do |strike, amount|
144
+ if amount < all[ outs[date]['summary']['value'] ]
145
+ outs[date]['summary']['value'] = strike
146
+ end
147
+ end
148
+ end
149
+
150
+ return outs
151
+ end
152
+
153
+
154
+ end
@@ -0,0 +1,149 @@
1
+
2
+ require 'distribution'
3
+ N = Distribution::Normal
4
+
5
+ module Iro::OptionBlackScholes
6
+
7
+ ##
8
+ ## black-scholes pricing
9
+ ##
10
+
11
+ =begin
12
+ ##
13
+ ##
14
+ ##
15
+ annual to daily:
16
+
17
+ AR = ((DR + 1)^365 – 1) x 100
18
+
19
+ ##
20
+ ##
21
+ ##
22
+ From: https://www.investopedia.com/articles/optioninvestor/07/options_beat_market.asp
23
+
24
+ K :: strike price
25
+ S_t :: last
26
+ r :: risk-free rate
27
+ t :: time to maturity
28
+
29
+ C = S_t N( d1 ) - K e^-rt N( d2 )
30
+
31
+ d1 = ln( St / K ) + (r + theta**2 / 2 )t
32
+ /{ theta_s * sqrt( t ) }
33
+
34
+ d2 = d1 - theta_s sqrt( t )
35
+
36
+ ##
37
+ ## From: https://en.wikipedia.org/wiki/Black%E2%80%93Scholes_model
38
+ ##
39
+
40
+ D :: e^(rt) # discount factor
41
+ F :: e^(rt) S # forward price of underlying
42
+
43
+ C(F,t) = D[ N(d1)F - N(d2)K ]
44
+
45
+ d1 = ln(F/K) + stdev**2 t / 2
46
+ /{ stdev sqrt(t) }
47
+ d2 = d1 - stdev sqrt(t)
48
+
49
+ ##
50
+ ## From: https://www.daytrading.com/options-pricing-models
51
+ ##
52
+ C0 = S0N(d1) – Xe-rtN(d2)
53
+
54
+ C0 = current call premium
55
+ S0 = current stock price
56
+ N(d1) = the probability that a value in a normal distribution will be less than d
57
+ N(d2) = the probability that the option will be in the money by expiration
58
+ X = strike price of the option
59
+ T = time until expiration (expressed in years)
60
+ r = risk-free interest rate
61
+ e = 2.71828, the base of the natural logarithm
62
+ ln = natural logarithm function
63
+ σ = standard deviation of the stock’s annualized rate of return (compounded continuously)
64
+ d1 = ln(S0/X) + (r + σ2/2)Tσ√T
65
+
66
+ d2 = d1 – σ√T
67
+
68
+ Note that:
69
+
70
+ Xe-rt = X/ert = the present value of the strike price using a continuously compounded interest rate
71
+
72
+ ##
73
+ ## From: https://www.wallstreetmojo.com/black-scholes-model/
74
+ ##
75
+
76
+
77
+ ## init
78
+ require 'distribution'
79
+ N = Distribution::Normal
80
+ stock = Iro::Stock.find_by ticker: 'NVDA'
81
+ strike = 910.0
82
+ r = Iro::Option.rate_daily
83
+ stdev = 91.0
84
+ t = 7.0
85
+ expires_on = '2024-03-22'
86
+
87
+ =end
88
+ def d1
89
+ last = stock.last
90
+ r = self.class.rate_annual
91
+
92
+ out = Math.log( last / strike ) + ( r + stdev**2 / 2 ) * t
93
+ out = out /( stdev * Math.sqrt(t) )
94
+ return out
95
+ end
96
+ def d2
97
+ last = stock.last
98
+ r = self.class.rate_annual
99
+
100
+ out = d1 - stdev * Math.sqrt( t )
101
+ return out
102
+ end
103
+ def t
104
+ # t = 1.0 / 365 * Date.today.business_days_until( expires_on )
105
+ t = 1.0 / 365 * (expires_on - Date.today).to_i
106
+ end
107
+ def stdev
108
+ recompute = nil
109
+ stock.stdev( recompute: recompute )
110
+ end
111
+ def call_price
112
+ last = stock.last
113
+ r = self.class.rate_annual
114
+
115
+ out = N.cdf( d1 ) * last - N.cdf( d2 ) * strike * Math::E**( -1 * r * t )
116
+ return out
117
+ end
118
+
119
+ def put_price
120
+ last = stock.last
121
+ r = self.class.rate_annual
122
+
123
+ out = N.cdf(-d2) * strike * exp(-r*t) - N.cdf(-d1) * last
124
+ return out
125
+ end
126
+
127
+
128
+ def self.rate_annual
129
+ 0.05
130
+ end
131
+
132
+ =begin
133
+ # test
134
+
135
+ inn = 100
136
+ n.times { inn = inn*(1.0+out) }
137
+ inn
138
+
139
+ =end
140
+ def self.rate_daily
141
+ n = 250.0 # days
142
+ # n = 12 # months
143
+
144
+ out = (1.0+self.rate_annual)**(1.0/n) - 1.0
145
+ puts! out, 'rate_daily'
146
+ return out
147
+ end
148
+
149
+ end