schwab_rb 0.2.0

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 (71) hide show
  1. checksums.yaml +7 -0
  2. data/.copilotignore +4 -0
  3. data/.rspec +2 -0
  4. data/.rspec_status +292 -0
  5. data/.rubocop.yml +41 -0
  6. data/.rubocop_todo.yml +105 -0
  7. data/CHANGELOG.md +28 -0
  8. data/LICENSE.txt +23 -0
  9. data/README.md +271 -0
  10. data/Rakefile +12 -0
  11. data/doc/notes/data_objects_analysis.md +223 -0
  12. data/doc/notes/data_objects_refactoring_plan.md +82 -0
  13. data/examples/fetch_account_numbers.rb +49 -0
  14. data/examples/fetch_user_preferences.rb +49 -0
  15. data/lib/schwab_rb/account.rb +9 -0
  16. data/lib/schwab_rb/auth/auth_context.rb +23 -0
  17. data/lib/schwab_rb/auth/init_client_easy.rb +45 -0
  18. data/lib/schwab_rb/auth/init_client_login.rb +201 -0
  19. data/lib/schwab_rb/auth/init_client_token_file.rb +30 -0
  20. data/lib/schwab_rb/auth/login_flow_server.rb +55 -0
  21. data/lib/schwab_rb/auth/token.rb +24 -0
  22. data/lib/schwab_rb/auth/token_manager.rb +105 -0
  23. data/lib/schwab_rb/clients/async_client.rb +122 -0
  24. data/lib/schwab_rb/clients/base_client.rb +887 -0
  25. data/lib/schwab_rb/clients/client.rb +97 -0
  26. data/lib/schwab_rb/configuration.rb +39 -0
  27. data/lib/schwab_rb/constants.rb +7 -0
  28. data/lib/schwab_rb/data_objects/account.rb +281 -0
  29. data/lib/schwab_rb/data_objects/account_numbers.rb +68 -0
  30. data/lib/schwab_rb/data_objects/instrument.rb +156 -0
  31. data/lib/schwab_rb/data_objects/market_hours.rb +275 -0
  32. data/lib/schwab_rb/data_objects/option.rb +147 -0
  33. data/lib/schwab_rb/data_objects/option_chain.rb +95 -0
  34. data/lib/schwab_rb/data_objects/option_expiration_chain.rb +134 -0
  35. data/lib/schwab_rb/data_objects/order.rb +186 -0
  36. data/lib/schwab_rb/data_objects/order_leg.rb +68 -0
  37. data/lib/schwab_rb/data_objects/order_preview.rb +237 -0
  38. data/lib/schwab_rb/data_objects/position.rb +100 -0
  39. data/lib/schwab_rb/data_objects/price_history.rb +187 -0
  40. data/lib/schwab_rb/data_objects/quote.rb +276 -0
  41. data/lib/schwab_rb/data_objects/transaction.rb +132 -0
  42. data/lib/schwab_rb/data_objects/user_preferences.rb +129 -0
  43. data/lib/schwab_rb/market_hours.rb +13 -0
  44. data/lib/schwab_rb/movers.rb +35 -0
  45. data/lib/schwab_rb/option.rb +64 -0
  46. data/lib/schwab_rb/orders/builder.rb +202 -0
  47. data/lib/schwab_rb/orders/destination.rb +19 -0
  48. data/lib/schwab_rb/orders/duration.rb +9 -0
  49. data/lib/schwab_rb/orders/equity_instructions.rb +10 -0
  50. data/lib/schwab_rb/orders/errors.rb +5 -0
  51. data/lib/schwab_rb/orders/instruments.rb +35 -0
  52. data/lib/schwab_rb/orders/option_instructions.rb +10 -0
  53. data/lib/schwab_rb/orders/order.rb +77 -0
  54. data/lib/schwab_rb/orders/price_link_basis.rb +15 -0
  55. data/lib/schwab_rb/orders/price_link_type.rb +9 -0
  56. data/lib/schwab_rb/orders/session.rb +14 -0
  57. data/lib/schwab_rb/orders/special_instruction.rb +10 -0
  58. data/lib/schwab_rb/orders/stop_price_link_basis.rb +15 -0
  59. data/lib/schwab_rb/orders/stop_price_link_type.rb +9 -0
  60. data/lib/schwab_rb/orders/stop_type.rb +11 -0
  61. data/lib/schwab_rb/orders/tax_lot_method.rb +13 -0
  62. data/lib/schwab_rb/price_history.rb +55 -0
  63. data/lib/schwab_rb/quote.rb +13 -0
  64. data/lib/schwab_rb/transaction.rb +23 -0
  65. data/lib/schwab_rb/utils/enum_enforcer.rb +73 -0
  66. data/lib/schwab_rb/utils/logger.rb +70 -0
  67. data/lib/schwab_rb/utils/redactor.rb +104 -0
  68. data/lib/schwab_rb/version.rb +5 -0
  69. data/lib/schwab_rb.rb +48 -0
  70. data/sig/schwab_rb.rbs +4 -0
  71. metadata +289 -0
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+ require_relative "base_client"
7
+ require_relative "../utils/logger"
8
+ require_relative "../utils/redactor"
9
+
10
+ module SchwabRb
11
+ class Client < BaseClient
12
+ BASE_URL = "https://api.schwabapi.com"
13
+
14
+ private
15
+
16
+ def get(path, params = {})
17
+ dest = URI(URI::DEFAULT_PARSER.escape("#{BASE_URL}#{path}"))
18
+ dest.query = URI.encode_www_form(params) if params.any?
19
+
20
+ req_num = req_num()
21
+ log_request("GET", req_num, dest, params)
22
+ response = session.get(dest)
23
+
24
+ log_response(response, req_num)
25
+ response
26
+ end
27
+
28
+ def post(path, data = {})
29
+ dest = URI(URI::DEFAULT_PARSER.escape("#{BASE_URL}#{path}"))
30
+
31
+ req_num = req_num()
32
+ log_request("POST", req_num, dest, data)
33
+
34
+ response = session.post(
35
+ dest,
36
+ {
37
+ :body => data.to_json,
38
+ :headers => { "Content-Type" => "application/json" }
39
+ }
40
+ )
41
+ log_response(response, req_num)
42
+ response
43
+ end
44
+
45
+ def put(path, data = {})
46
+ dest = URI(URI::DEFAULT_PARSER.escape("#{BASE_URL}#{path}"))
47
+
48
+ req_num = req_num()
49
+ log_request("PUT", req_num, dest, data)
50
+
51
+ response = session.put(
52
+ dest,
53
+ {
54
+ :body => data.to_json,
55
+ :headers => { "Content-Type" => "application/json" }
56
+ }
57
+ )
58
+ log_response(response, req_num)
59
+ response
60
+ end
61
+
62
+ def delete(path)
63
+ dest = URI(URI::DEFAULT_PARSER.escape("#{BASE_URL}#{path}"))
64
+
65
+ req_num = req_num()
66
+ log_request("DELETE", req_num, dest)
67
+
68
+ response = session.delete(dest)
69
+ log_response(response, req_num)
70
+ response
71
+ end
72
+
73
+ def log_request(method, req_num, dest, data = nil)
74
+ redacted_dest = SchwabRb::Redactor.redact_url(dest.to_s)
75
+ SchwabRb::Logger.logger.info("Req #{req_num}: #{method} to #{redacted_dest}")
76
+
77
+ if data
78
+ redacted_data = SchwabRb::Redactor.redact_data(data)
79
+ SchwabRb::Logger.logger.debug("Payload: #{JSON.pretty_generate(redacted_data)}")
80
+ end
81
+ end
82
+
83
+ def log_response(response, req_num)
84
+ SchwabRb::Logger.logger.info("Resp #{req_num}: Status #{response.status}")
85
+
86
+ if SchwabRb::Logger.logger.level == ::Logger::DEBUG
87
+ redacted_body = SchwabRb::Redactor.redact_response_body(response)
88
+ SchwabRb::Logger.logger.debug("Response body: #{JSON.pretty_generate(redacted_body)}") if redacted_body
89
+ end
90
+ end
91
+
92
+ def req_num
93
+ @request_counter ||= 0
94
+ @request_counter += 1
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,39 @@
1
+ module SchwabRb
2
+ class Configuration
3
+ attr_accessor :logger, :log_file, :log_level, :silence_output
4
+
5
+ def initialize
6
+ @logger = nil
7
+ @log_file = ENV['SCHWAB_LOGFILE']
8
+ @log_level = ENV.fetch('SCHWAB_LOG_LEVEL', 'WARN').upcase
9
+ @silence_output = ENV.fetch('SCHWAB_SILENCE_OUTPUT', 'false').downcase == 'true'
10
+ end
11
+
12
+ def has_external_logger?
13
+ !@logger.nil?
14
+ end
15
+
16
+ def should_create_logger?
17
+ !has_external_logger? && !@silence_output
18
+ end
19
+
20
+ def effective_log_file
21
+ @log_file || (ENV['LOGFILE'] if ENV['LOGFILE'] && !ENV['LOGFILE'].empty?)
22
+ end
23
+ end
24
+
25
+ class << self
26
+ def configuration
27
+ @configuration ||= Configuration.new
28
+ end
29
+
30
+ def configure
31
+ yield(configuration) if block_given?
32
+ SchwabRb::Logger.reset! if defined?(SchwabRb::Logger)
33
+ end
34
+
35
+ def reset_configuration!
36
+ @configuration = Configuration.new
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SchwabRb::Constants
4
+ SCHWAB_BASE_URL="https://api.schwabapi.com"
5
+ TOKEN_ENDPOINT="https://api.schwabapi.com/v1/oauth/token"
6
+ DEFAULT_TOKEN_PATH="./token.json"
7
+ end
@@ -0,0 +1,281 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'instrument'
4
+ require_relative 'position'
5
+
6
+ module SchwabRb
7
+ module DataObjects
8
+ class InitialBalances
9
+ attr_reader :accrued_interest, :cash_balance,
10
+ :cash_receipts, :long_option_market_value,
11
+ :liquidation_value, :money_market_fund,
12
+ :available_funds_non_marginable_trade,
13
+ :bond_value, :buying_power, :cash_available_for_trading,
14
+ :day_trading_buying_power, :day_trading_buying_power_call,
15
+ :day_trading_equity_call, :equity, :equity_percentage,
16
+ :long_margin_value, :long_stock_value, :maintenance_call, :maintenance_requirement,
17
+ :margin, :margin_equity, :mutual_fund_value, :reg_t_call, :short_margin_value,
18
+ :short_option_market_value, :short_stock_value, :total_cash, :is_in_call,
19
+ :pending_deposits, :margin_balance, :short_balance, :account_value
20
+
21
+ class << self
22
+ def build(data)
23
+ new(
24
+ accrued_interest: data[:accruedInterest],
25
+ cash_balance: data[:cashBalance],
26
+ cash_receipts: data[:cashReceipts],
27
+ long_option_market_value: data[:longOptionMarketValue],
28
+ liquidation_value: data[:liquidationValue],
29
+ money_market_fund: data[:moneyMarketFund],
30
+ available_funds_non_marginable_trade: data[:availableFundsNonMarginableTrade],
31
+ bond_value: data[:bondValue],
32
+ buying_power: data[:buyingPower],
33
+ cash_available_for_trading: data[:cashAvailableForTrading],
34
+ day_trading_buying_power: data[:dayTradingBuyingPower],
35
+ day_trading_buying_power_call: data[:dayTradingBuyingPowerCall],
36
+ day_trading_equity_call: data[:dayTradingEquityCall],
37
+ equity: data[:equity],
38
+ equity_percentage: data[:equityPercentage],
39
+ long_margin_value: data[:longMarginValue],
40
+ long_stock_value: data[:longStockValue],
41
+ maintenance_call: data[:maintenanceCall],
42
+ maintenance_requirement: data[:maintenanceRequirement],
43
+ margin: data[:margin],
44
+ margin_equity: data[:marginEquity],
45
+ mutual_fund_value: data[:mutualFundValue],
46
+ reg_t_call: data[:regTCall],
47
+ short_margin_value: data[:shortMarginValue],
48
+ short_option_market_value: data[:shortOptionMarketValue],
49
+ short_stock_value: data[:shortStockValue],
50
+ total_cash: data[:totalCash],
51
+ is_in_call: data[:isInCall],
52
+ pending_deposits: data[:pendingDeposits],
53
+ margin_balance: data[:marginBalance],
54
+ short_balance: data[:shortBalance],
55
+ account_value: data[:accountValue]
56
+ )
57
+ end
58
+ end
59
+
60
+ def initialize(
61
+ accrued_interest:, cash_balance:, cash_receipts:, long_option_market_value:, liquidation_value:,
62
+ money_market_fund:, available_funds_non_marginable_trade:, bond_value:, buying_power:,
63
+ cash_available_for_trading:, day_trading_buying_power:, day_trading_buying_power_call:,
64
+ day_trading_equity_call:, equity:, equity_percentage:, long_margin_value:, long_stock_value:,
65
+ maintenance_call:, maintenance_requirement:, margin:, margin_equity:, mutual_fund_value:,
66
+ reg_t_call:, short_margin_value:, short_option_market_value:, short_stock_value:, total_cash:,
67
+ is_in_call:, pending_deposits:, margin_balance:, short_balance:, account_value:
68
+ )
69
+ @accrued_interest = accrued_interest
70
+ @cash_balance = cash_balance
71
+ @cash_receipts = cash_receipts
72
+ @long_option_market_value = long_option_market_value
73
+ @liquidation_value = liquidation_value
74
+ @money_market_fund = money_market_fund
75
+ @available_funds_non_marginable_trade = available_funds_non_marginable_trade
76
+ @bond_value = bond_value
77
+ @buying_power = buying_power
78
+ @cash_available_for_trading = cash_available_for_trading
79
+ @day_trading_buying_power = day_trading_buying_power
80
+ @day_trading_buying_power_call = day_trading_buying_power_call
81
+ @day_trading_equity_call = day_trading_equity_call
82
+ @equity = equity
83
+ @equity_percentage = equity_percentage
84
+ @long_margin_value = long_margin_value
85
+ @long_stock_value = long_stock_value
86
+ @maintenance_call = maintenance_call
87
+ @maintenance_requirement = maintenance_requirement
88
+ @margin = margin
89
+ @margin_equity = margin_equity
90
+ @mutual_fund_value = mutual_fund_value
91
+ @reg_t_call = reg_t_call
92
+ @short_margin_value = short_margin_value
93
+ @short_option_market_value = short_option_market_value
94
+ @short_stock_value = short_stock_value
95
+ @total_cash = total_cash
96
+ @is_in_call = is_in_call
97
+ @pending_deposits = pending_deposits
98
+ @margin_balance = margin_balance
99
+ @short_balance = short_balance
100
+ @account_value = account_value
101
+ end
102
+ end
103
+
104
+ class CurrentBalances
105
+ attr_reader :accrued_interest, :cash_balance, :cash_receipts, :long_option_market_value,
106
+ :liquidation_value, :long_market_value, :money_market_fund, :savings,
107
+ :short_market_value, :pending_deposits, :mutual_fund_value, :bond_value,
108
+ :short_option_market_value, :available_funds, :available_funds_non_marginable_trade,
109
+ :buying_power, :buying_power_non_marginable_trade, :day_trading_buying_power,
110
+ :equity, :equity_percentage, :long_margin_value, :maintenance_call,
111
+ :maintenance_requirement, :margin_balance, :reg_t_call, :short_balance,
112
+ :short_margin_value, :sma
113
+
114
+ class << self
115
+ def build(data)
116
+ new(
117
+ accrued_interest: data[:accruedInterest],
118
+ cash_balance: data[:cashBalance],
119
+ cash_receipts: data[:cashReceipts],
120
+ long_option_market_value: data[:longOptionMarketValue],
121
+ liquidation_value: data[:liquidationValue],
122
+ long_market_value: data[:longMarketValue],
123
+ money_market_fund: data[:moneyMarketFund],
124
+ savings: data[:savings],
125
+ short_market_value: data[:shortMarketValue],
126
+ pending_deposits: data[:pendingDeposits],
127
+ mutual_fund_value: data[:mutualFundValue],
128
+ bond_value: data[:bondValue],
129
+ short_option_market_value: data[:shortOptionMarketValue],
130
+ available_funds: data[:availableFunds],
131
+ available_funds_non_marginable_trade: data[:availableFundsNonMarginableTrade],
132
+ buying_power: data[:buyingPower],
133
+ buying_power_non_marginable_trade: data[:buyingPowerNonMarginableTrade],
134
+ day_trading_buying_power: data[:dayTradingBuyingPower],
135
+ equity: data[:equity],
136
+ equity_percentage: data[:equityPercentage],
137
+ long_margin_value: data[:longMarginValue],
138
+ maintenance_call: data[:maintenanceCall],
139
+ maintenance_requirement: data[:maintenanceRequirement],
140
+ margin_balance: data[:marginBalance],
141
+ reg_t_call: data[:regTCall],
142
+ short_balance: data[:shortBalance],
143
+ short_margin_value: data[:shortMarginValue],
144
+ sma: data[:sma]
145
+ )
146
+ end
147
+ end
148
+
149
+ def initialize(
150
+ accrued_interest:, cash_balance:, cash_receipts:, long_option_market_value:, liquidation_value:,
151
+ long_market_value:, money_market_fund:, savings:, short_market_value:, pending_deposits:,
152
+ mutual_fund_value:, bond_value:, short_option_market_value:, available_funds:,
153
+ available_funds_non_marginable_trade:, buying_power:, buying_power_non_marginable_trade:,
154
+ day_trading_buying_power:, equity:, equity_percentage:, long_margin_value:, maintenance_call:,
155
+ maintenance_requirement:, margin_balance:, reg_t_call:, short_balance:, short_margin_value:,
156
+ sma:
157
+ )
158
+ @accrued_interest = accrued_interest
159
+ @cash_balance = cash_balance
160
+ @cash_receipts = cash_receipts
161
+ @long_option_market_value = long_option_market_value
162
+ @liquidation_value = liquidation_value
163
+ @long_market_value = long_market_value
164
+ @money_market_fund = money_market_fund
165
+ @savings = savings
166
+ @short_market_value = short_market_value
167
+ @pending_deposits = pending_deposits
168
+ @mutual_fund_value = mutual_fund_value
169
+ @bond_value = bond_value
170
+ @short_option_market_value = short_option_market_value
171
+ @available_funds = available_funds
172
+ @available_funds_non_marginable_trade = available_funds_non_marginable_trade
173
+ @buying_power = buying_power
174
+ @buying_power_non_marginable_trade = buying_power_non_marginable_trade
175
+ @day_trading_buying_power = day_trading_buying_power
176
+ @equity = equity
177
+ @equity_percentage = equity_percentage
178
+ @long_margin_value = long_margin_value
179
+ @maintenance_call = maintenance_call
180
+ @maintenance_requirement = maintenance_requirement
181
+ @margin_balance = margin_balance
182
+ @reg_t_call = reg_t_call
183
+ @short_balance = short_balance
184
+ @short_margin_value = short_margin_value
185
+ @sma = sma
186
+ end
187
+ end
188
+
189
+ class ProjectedBalances
190
+ attr_reader :available_funds, :available_funds_non_marginable_trade, :buying_power,
191
+ :day_trading_buying_power, :day_trading_buying_power_call, :maintenance_call,
192
+ :reg_t_call, :is_in_call, :stock_buying_power
193
+
194
+ class << self
195
+ def build(data)
196
+ new(
197
+ available_funds: data[:availableFunds],
198
+ available_funds_non_marginable_trade: data[:availableFundsNonMarginableTrade],
199
+ buying_power: data[:buyingPower],
200
+ day_trading_buying_power: data[:dayTradingBuyingPower],
201
+ day_trading_buying_power_call: data[:dayTradingBuyingPowerCall],
202
+ maintenance_call: data[:maintenanceCall],
203
+ reg_t_call: data[:regTCall],
204
+ is_in_call: data[:isInCall],
205
+ stock_buying_power: data[:stockBuyingPower]
206
+ )
207
+ end
208
+ end
209
+
210
+ def initialize(
211
+ available_funds:, available_funds_non_marginable_trade:, buying_power:, day_trading_buying_power:,
212
+ day_trading_buying_power_call:, maintenance_call:, reg_t_call:, is_in_call:, stock_buying_power:
213
+ )
214
+ @available_funds = available_funds
215
+ @available_funds_non_marginable_trade = available_funds_non_marginable_trade
216
+ @buying_power = buying_power
217
+ @day_trading_buying_power = day_trading_buying_power
218
+ @day_trading_buying_power_call = day_trading_buying_power_call
219
+ @maintenance_call = maintenance_call
220
+ @reg_t_call = reg_t_call
221
+ @is_in_call = is_in_call
222
+ @stock_buying_power = stock_buying_power
223
+ end
224
+ end
225
+
226
+ class AggregatedBalance
227
+ class << self
228
+ def build(data)
229
+ new(
230
+ current_liquidation_value: data.fetch(:currentLiquidationValue),
231
+ liquidation_value: data.fetch(:liquidationValue)
232
+ )
233
+ end
234
+ end
235
+
236
+ def initialize(current_liquidation_value:, liquidation_value:)
237
+ @current_liquidation_value = current_liquidation_value
238
+ @liquidation_value = liquidation_value
239
+ end
240
+
241
+ attr_reader :current_liquidation_value, :liquidation_value
242
+ end
243
+
244
+ class Account
245
+ class << self
246
+ def build(data)
247
+ data = data[:securitiesAccount] if data.key?(:securitiesAccount)
248
+ new(
249
+ type: data.fetch(:type),
250
+ account_number: data.fetch(:accountNumber),
251
+ round_trips: data.fetch(:roundTrips),
252
+ is_day_trader: data.fetch(:isDayTrader),
253
+ is_closing_only_restricted: data.fetch(:isClosingOnlyRestricted),
254
+ pfcb_flag: data.fetch(:pfcbFlag),
255
+ positions: data.fetch(:positions).map { |position| Position.build(position) },
256
+ initial_balances: InitialBalances.build(data.fetch(:initialBalances)),
257
+ current_balances: CurrentBalances.build(data.fetch(:currentBalances)),
258
+ projected_balances: ProjectedBalances.build(data.fetch(:projectedBalances))
259
+ )
260
+ end
261
+ end
262
+
263
+ def initialize(type:, account_number:, round_trips:, is_day_trader:, is_closing_only_restricted:,
264
+ pfcb_flag:, initial_balances:, current_balances:, projected_balances:, positions: [])
265
+ @type = type
266
+ @account_number = account_number
267
+ @round_trips = round_trips
268
+ @is_day_trader = is_day_trader
269
+ @is_closing_only_restricted = is_closing_only_restricted
270
+ @pfcb_flag = pfcb_flag
271
+ @positions = positions
272
+ @initial_balances = initial_balances
273
+ @current_balances = current_balances
274
+ @projected_balances = projected_balances
275
+ end
276
+
277
+ attr_reader :type, :account_number, :round_trips, :is_day_trader, :is_closing_only_restricted,
278
+ :pfcb_flag, :positions, :initial_balances, :current_balances, :projected_balances
279
+ end
280
+ end
281
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SchwabRb
4
+ module DataObjects
5
+ class AccountNumbers
6
+ attr_reader :accounts
7
+
8
+ class << self
9
+ def build(data)
10
+ new(data)
11
+ end
12
+ end
13
+
14
+ def initialize(data)
15
+ @accounts = data.map { |account_data| AccountNumber.new(account_data) }
16
+ end
17
+
18
+ def to_h
19
+ @accounts.map(&:to_h)
20
+ end
21
+
22
+ def find_by_account_number(account_number)
23
+ @accounts.find { |account| account.account_number == account_number }
24
+ end
25
+
26
+ def find_hash_value(account_number)
27
+ account = find_by_account_number(account_number)
28
+ account&.hash_value
29
+ end
30
+
31
+ def account_numbers
32
+ @accounts.map(&:account_number)
33
+ end
34
+
35
+ def hash_values
36
+ @accounts.map(&:hash_value)
37
+ end
38
+
39
+ def size
40
+ @accounts.size
41
+ end
42
+
43
+ def empty?
44
+ @accounts.empty?
45
+ end
46
+
47
+ def each(&block)
48
+ @accounts.each(&block)
49
+ end
50
+
51
+ class AccountNumber
52
+ attr_reader :account_number, :hash_value
53
+
54
+ def initialize(data)
55
+ @account_number = data[:accountNumber]
56
+ @hash_value = data[:hashValue]
57
+ end
58
+
59
+ def to_h
60
+ {
61
+ accountNumber: @account_number,
62
+ hashValue: @hash_value
63
+ }
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SchwabRb
4
+ module DataObjects
5
+ class Asset
6
+ attr_reader :asset_type, :status, :symbol, :instrument_id, :closing_price, :type, :description,
7
+ :active_contract, :expiration_date, :last_trading_date, :multiplier, :future_type
8
+
9
+ def self.build(data)
10
+ new(
11
+ asset_type: data.fetch(:assetType, nil),
12
+ status: data.fetch(:status, nil),
13
+ symbol: data.fetch(:symbol, nil),
14
+ instrument_id: data.fetch(:instrumentId, nil),
15
+ closing_price: data.fetch(:closingPrice, nil),
16
+ type: data.fetch(:type, nil),
17
+ description: data.fetch(:description, nil),
18
+ active_contract: data.fetch(:activeContract, nil),
19
+ expiration_date: data.fetch(:expirationDate, nil),
20
+ last_trading_date: data.fetch(:lastTradingDate, nil),
21
+ multiplier: data.fetch(:multiplier, nil),
22
+ future_type: data.fetch(:futureType, nil)
23
+ )
24
+ end
25
+
26
+ def initialize(
27
+ asset_type: nil, status: nil, symbol: nil, instrument_id: nil, closing_price: nil, type: nil, description: nil, active_contract: nil, expiration_date: nil, last_trading_date: nil, multiplier: nil, future_type: nil
28
+ )
29
+ @asset_type = asset_type
30
+ @status = status
31
+ @symbol = symbol
32
+ @instrument_id = instrument_id
33
+ @closing_price = closing_price
34
+ @type = type
35
+ @description = description
36
+ @active_contract = active_contract
37
+ @expiration_date = expiration_date
38
+ @last_trading_date = last_trading_date
39
+ @multiplier = multiplier
40
+ @future_type = future_type
41
+ end
42
+
43
+ def to_h
44
+ {
45
+ assetType: @asset_type,
46
+ status: @status,
47
+ symbol: @symbol,
48
+ instrumentId: @instrument_id,
49
+ closingPrice: @closing_price,
50
+ type: @type,
51
+ description: @description,
52
+ activeContract: @active_contract,
53
+ expirationDate: @expiration_date,
54
+ lastTradingDate: @last_trading_date,
55
+ multiplier: @multiplier,
56
+ futureType: @future_type
57
+ }.compact
58
+ end
59
+ end
60
+
61
+ class OptionDeliverable
62
+ attr_reader :root_symbol, :symbol, :deliverable_units, :deliverable_number, :strike_percent, :deliverable
63
+
64
+ def self.build(data)
65
+ new(
66
+ root_symbol: data.fetch(:rootSymbol, nil),
67
+ symbol: data.fetch(:symbol, nil),
68
+ strike_percent: data.fetch(:strikePercent, nil),
69
+ deliverable_number: data.fetch(:deliverableNumber, nil),
70
+ deliverable_units: data.fetch(:deliverableUnits),
71
+ deliverable: data.fetch(:deliverable, nil).then { |d| d.nil? ? nil : Instrument.build(d) }
72
+ )
73
+ end
74
+
75
+ def initialize(root_symbol:, symbol:, deliverable_units:, deliverable_number:, strike_percent:, deliverable:)
76
+ @root_symbol = root_symbol
77
+ @symbol = symbol
78
+ @deliverable_units = deliverable_units
79
+ @deliverable_number = deliverable_number
80
+ @strike_percent = strike_percent
81
+ @deliverable = deliverable
82
+ end
83
+
84
+ def to_h
85
+ {
86
+ rootSymbol: @root_symbol,
87
+ symbol: @symbol,
88
+ strikePercent: @strike_percent,
89
+ deliverableNumber: @deliverable_number,
90
+ deliverableUnits: @deliverable_units,
91
+ deliverable: @deliverable&.to_h
92
+ }.compact
93
+ end
94
+ end
95
+
96
+ class Instrument
97
+ attr_reader :asset_type, :cusip, :symbol, :description, :net_change, :type, :put_call,
98
+ :underlying_symbol, :status, :instrument_id, :closing_price, :option_deliverables
99
+
100
+ def self.build(data)
101
+ new(
102
+ asset_type: data.fetch(:assetType),
103
+ symbol: data.fetch(:symbol, nil),
104
+ description: data.fetch(:description, nil),
105
+ cusip: data.fetch(:cusip, nil),
106
+ net_change: data.fetch(:netChange, nil),
107
+ type: data.fetch(:type, nil),
108
+ put_call: data.fetch(:putCall, nil),
109
+ underlying_symbol: data.fetch(:underlyingSymbol, nil),
110
+ status: data.fetch(:status, nil),
111
+ instrument_id: data.fetch(:instrumentId, nil),
112
+ closing_price: data.fetch(:closingPrice, nil),
113
+ option_deliverables: data.fetch(:optionDeliverables, []).map { |d| OptionDeliverable.build(d) }
114
+ )
115
+ end
116
+
117
+ def initialize(
118
+ symbol:, description:, asset_type: nil, cusip: nil, net_change: nil, type: nil, put_call: nil, underlying_symbol: nil, status: nil, instrument_id: nil, closing_price: nil, option_deliverables: []
119
+ )
120
+ @asset_type = asset_type
121
+ @cusip = cusip
122
+ @symbol = symbol
123
+ @description = description
124
+ @net_change = net_change
125
+ @type = type
126
+ @put_call = put_call
127
+ @underlying_symbol = underlying_symbol
128
+ @status = status
129
+ @instrument_id = instrument_id
130
+ @closing_price = closing_price
131
+ @option_deliverables = option_deliverables
132
+ end
133
+
134
+ def option?
135
+ asset_type == 'OPTION'
136
+ end
137
+
138
+ def to_h
139
+ {
140
+ assetType: @asset_type,
141
+ symbol: @symbol,
142
+ description: @description,
143
+ cusip: @cusip,
144
+ netChange: @net_change,
145
+ type: @type,
146
+ putCall: @put_call,
147
+ underlyingSymbol: @underlying_symbol,
148
+ status: @status,
149
+ instrumentId: @instrument_id,
150
+ closingPrice: @closing_price,
151
+ optionDeliverables: @option_deliverables.map(&:to_h)
152
+ }.compact
153
+ end
154
+ end
155
+ end
156
+ end