wco_models 3.1.0.184 → 3.1.0.188

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