schwab_rb 0.3.2 → 0.3.4

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec_status +200 -200
  3. data/.rubocop.yml +2 -0
  4. data/examples/fetch_account_numbers.rb +1 -1
  5. data/examples/fetch_user_preferences.rb +1 -1
  6. data/lib/schwab_rb/auth/auth_context.rb +19 -15
  7. data/lib/schwab_rb/auth/init_client_easy.rb +36 -32
  8. data/lib/schwab_rb/auth/init_client_login.rb +165 -161
  9. data/lib/schwab_rb/auth/init_client_token_file.rb +27 -23
  10. data/lib/schwab_rb/auth/login_flow_server.rb +39 -36
  11. data/lib/schwab_rb/auth/token.rb +25 -21
  12. data/lib/schwab_rb/auth/token_manager.rb +88 -84
  13. data/lib/schwab_rb/clients/async_client.rb +2 -0
  14. data/lib/schwab_rb/clients/base_client.rb +2 -0
  15. data/lib/schwab_rb/configuration.rb +2 -0
  16. data/lib/schwab_rb/constants.rb +6 -4
  17. data/lib/schwab_rb/data_objects/account_numbers.rb +1 -0
  18. data/lib/schwab_rb/data_objects/market_hours.rb +1 -1
  19. data/lib/schwab_rb/data_objects/market_movers.rb +1 -0
  20. data/lib/schwab_rb/data_objects/option_expiration_chain.rb +1 -1
  21. data/lib/schwab_rb/data_objects/order_preview.rb +21 -49
  22. data/lib/schwab_rb/data_objects/price_history.rb +1 -1
  23. data/lib/schwab_rb/data_objects/transaction.rb +8 -0
  24. data/lib/schwab_rb/orders/builder.rb +162 -158
  25. data/lib/schwab_rb/orders/destination.rb +2 -0
  26. data/lib/schwab_rb/orders/duration.rb +14 -8
  27. data/lib/schwab_rb/orders/equity_instructions.rb +2 -0
  28. data/lib/schwab_rb/orders/errors.rb +2 -0
  29. data/lib/schwab_rb/orders/instruments.rb +2 -0
  30. data/lib/schwab_rb/orders/option_instructions.rb +2 -0
  31. data/lib/schwab_rb/orders/order.rb +2 -0
  32. data/lib/schwab_rb/orders/price_link_basis.rb +2 -0
  33. data/lib/schwab_rb/orders/price_link_type.rb +2 -0
  34. data/lib/schwab_rb/orders/session.rb +16 -10
  35. data/lib/schwab_rb/orders/special_instruction.rb +2 -0
  36. data/lib/schwab_rb/orders/stop_price_link_basis.rb +2 -0
  37. data/lib/schwab_rb/orders/stop_price_link_type.rb +2 -0
  38. data/lib/schwab_rb/orders/stop_type.rb +2 -0
  39. data/lib/schwab_rb/orders/tax_lot_method.rb +2 -0
  40. data/lib/schwab_rb/utils/enum_enforcer.rb +3 -4
  41. data/lib/schwab_rb/utils/logger.rb +3 -1
  42. data/lib/schwab_rb/utils/redactor.rb +3 -5
  43. data/lib/schwab_rb/version.rb +1 -1
  44. metadata +3 -3
  45. /data/.claude/{settings.local.json → settings.json} +0 -0
@@ -1,24 +1,28 @@
1
- module SchwabRb::Auth
2
- class Token
3
- def initialize(
4
- token: nil,
5
- expires_in: nil,
6
- token_type: "Bearer",
7
- scope: nil,
8
- refresh_token: nil,
9
- id_token: nil,
10
- expires_at: nil
11
- )
12
- @token = token
13
- @expires_in = expires_in
14
- @token_type = token_type
15
- @scope = scope
16
- @refresh_token = refresh_token
17
- @id_token = id_token
18
- @expires_at = expires_at
19
- end
1
+ # frozen_string_literal: true
2
+
3
+ module SchwabRb
4
+ module Auth
5
+ class Token
6
+ def initialize(
7
+ token: nil,
8
+ expires_in: nil,
9
+ token_type: "Bearer",
10
+ scope: nil,
11
+ refresh_token: nil,
12
+ id_token: nil,
13
+ expires_at: nil
14
+ )
15
+ @token = token
16
+ @expires_in = expires_in
17
+ @token_type = token_type
18
+ @scope = scope
19
+ @refresh_token = refresh_token
20
+ @id_token = id_token
21
+ @expires_at = expires_at
22
+ end
20
23
 
21
- attr_reader :token, :expires_in, :token_type, :scope,
22
- :refresh_token, :id_token, :expires_at
24
+ attr_reader :token, :expires_in, :token_type, :scope,
25
+ :refresh_token, :id_token, :expires_at
26
+ end
23
27
  end
24
28
  end
@@ -1,103 +1,107 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "oauth2"
2
4
  require "json"
3
5
 
4
- module SchwabRb::Auth
5
- class TokenManager
6
- class << self
7
- def from_file(token_path)
8
- token_data = JSON.parse(File.read(token_path))
9
- token = SchwabRb::Auth::Token.new(
10
- token: token_data["token"]["access_token"],
11
- expires_in: token_data["token"]["expires_in"],
12
- token_type: token_data["token"]["token_type"],
13
- scope: token_data["token"]["scope"],
14
- refresh_token: token_data["token"]["refresh_token"],
15
- id_token: token_data["token"]["id_token"],
16
- expires_at: token_data["token"]["expires_at"]
17
- )
6
+ module SchwabRb
7
+ module Auth
8
+ class TokenManager
9
+ class << self
10
+ def from_file(token_path)
11
+ token_data = JSON.parse(File.read(token_path))
12
+ token = SchwabRb::Auth::Token.new(
13
+ token: token_data["token"]["access_token"],
14
+ expires_in: token_data["token"]["expires_in"],
15
+ token_type: token_data["token"]["token_type"],
16
+ scope: token_data["token"]["scope"],
17
+ refresh_token: token_data["token"]["refresh_token"],
18
+ id_token: token_data["token"]["id_token"],
19
+ expires_at: token_data["token"]["expires_at"]
20
+ )
18
21
 
19
- TokenManager.new(token, token_data["timestamp"], token_path: token_path)
20
- end
22
+ TokenManager.new(token, token_data["timestamp"], token_path: token_path)
23
+ end
21
24
 
22
- def from_oauth2_token(oauth2_token, timestamp, token_path: SchwabRb::Constants::DEFAULT_TOKEN_PATH)
23
- token = SchwabRb::Auth::Token.new(
24
- token: oauth2_token.token,
25
- expires_in: oauth2_token.expires_in,
26
- token_type: oauth2_token.params["token_type"] || "Bearer",
27
- scope: oauth2_token.params["scope"],
28
- refresh_token: oauth2_token.refresh_token,
29
- id_token: oauth2_token.params["id_token"],
30
- expires_at: oauth2_token.expires_at
31
- )
25
+ def from_oauth2_token(oauth2_token, timestamp, token_path: SchwabRb::Constants::DEFAULT_TOKEN_PATH)
26
+ token = SchwabRb::Auth::Token.new(
27
+ token: oauth2_token.token,
28
+ expires_in: oauth2_token.expires_in,
29
+ token_type: oauth2_token.params["token_type"] || "Bearer",
30
+ scope: oauth2_token.params["scope"],
31
+ refresh_token: oauth2_token.refresh_token,
32
+ id_token: oauth2_token.params["id_token"],
33
+ expires_at: oauth2_token.expires_at
34
+ )
32
35
 
33
- TokenManager.new(token, timestamp, token_path: token_path)
36
+ TokenManager.new(token, timestamp, token_path: token_path)
37
+ end
34
38
  end
35
- end
36
39
 
37
- def initialize(token, timestamp, token_path: SchwabRb::Constants::DEFAULT_TOKEN_PATH)
38
- @token = token
39
- @timestamp = timestamp
40
- @token_path = token_path
41
- end
40
+ def initialize(token, timestamp, token_path: SchwabRb::Constants::DEFAULT_TOKEN_PATH)
41
+ @token = token
42
+ @timestamp = timestamp
43
+ @token_path = token_path
44
+ end
42
45
 
43
- attr_reader :token, :timestamp, :token_path
44
-
45
- def refresh_token(client)
46
- new_token = client.session.refresh!
47
-
48
- @token = SchwabRb::Auth::Token.new(
49
- token: new_token.token,
50
- expires_in: new_token.expires_in,
51
- token_type: new_token.params["token_type"] || "Bearer",
52
- scope: new_token.params["scope"],
53
- refresh_token: new_token.refresh_token,
54
- id_token: new_token.params["id_token"],
55
- expires_at: new_token.expires_at
56
- )
57
- @timestamp = Time.now.to_i
58
-
59
- to_file
60
-
61
- oauth = OAuth2::Client.new(
62
- client.api_key,
63
- client.app_secret,
64
- site: SchwabRb::Constants::SCHWAB_BASE_URL,
65
- token_url: "/v1/oauth/token"
66
- )
67
-
68
- OAuth2::AccessToken.new(
69
- oauth,
70
- token.token,
71
- refresh_token: token.refresh_token,
72
- expires_at: token.expires_at
73
- )
74
- end
46
+ attr_reader :token, :timestamp, :token_path
75
47
 
76
- def to_file
77
- File.write(token_path, to_json)
78
- end
48
+ def refresh_token(client)
49
+ new_token = client.session.refresh!
79
50
 
80
- def token_age
81
- Time.now.to_i - timestamp
82
- end
51
+ @token = SchwabRb::Auth::Token.new(
52
+ token: new_token.token,
53
+ expires_in: new_token.expires_in,
54
+ token_type: new_token.params["token_type"] || "Bearer",
55
+ scope: new_token.params["scope"],
56
+ refresh_token: new_token.refresh_token,
57
+ id_token: new_token.params["id_token"],
58
+ expires_at: new_token.expires_at
59
+ )
60
+ @timestamp = Time.now.to_i
61
+
62
+ to_file
83
63
 
84
- def to_h
85
- {
86
- timestamp: timestamp,
87
- token: {
88
- expires_in: token.expires_in,
89
- token_type: token.token_type,
90
- scope: token.scope,
64
+ oauth = OAuth2::Client.new(
65
+ client.api_key,
66
+ client.app_secret,
67
+ site: SchwabRb::Constants::SCHWAB_BASE_URL,
68
+ token_url: "/v1/oauth/token"
69
+ )
70
+
71
+ OAuth2::AccessToken.new(
72
+ oauth,
73
+ token.token,
91
74
  refresh_token: token.refresh_token,
92
- access_token: token.token,
93
- id_token: token.id_token,
94
75
  expires_at: token.expires_at
76
+ )
77
+ end
78
+
79
+ def to_file
80
+ File.write(token_path, to_json)
81
+ end
82
+
83
+ def token_age
84
+ Time.now.to_i - timestamp
85
+ end
86
+
87
+ def to_h
88
+ {
89
+ timestamp: timestamp,
90
+ token: {
91
+ expires_in: token.expires_in,
92
+ token_type: token.token_type,
93
+ scope: token.scope,
94
+ refresh_token: token.refresh_token,
95
+ access_token: token.token,
96
+ id_token: token.id_token,
97
+ expires_at: token.expires_at
98
+ }
95
99
  }
96
- }
97
- end
100
+ end
98
101
 
99
- def to_json(*_args)
100
- to_h.to_json
102
+ def to_json(*_args)
103
+ to_h.to_json
104
+ end
101
105
  end
102
106
  end
103
107
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "async"
2
4
  require "async/http"
3
5
  require "json"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "date"
2
4
  require "json"
3
5
  require_relative "../utils/enum_enforcer"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SchwabRb
2
4
  class Configuration
3
5
  attr_accessor :logger, :log_file, :log_level, :silence_output
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
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"
3
+ module SchwabRb
4
+ module Constants
5
+ SCHWAB_BASE_URL="https://api.schwabapi.com"
6
+ TOKEN_ENDPOINT="https://api.schwabapi.com/v1/oauth/token"
7
+ DEFAULT_TOKEN_PATH="./token.json"
8
+ end
7
9
  end
@@ -3,6 +3,7 @@
3
3
  module SchwabRb
4
4
  module DataObjects
5
5
  class AccountNumbers
6
+ include Enumerable
6
7
  attr_reader :accounts
7
8
 
8
9
  class << self
@@ -230,7 +230,7 @@ module SchwabRb
230
230
  private
231
231
 
232
232
  def parse_session_periods(periods_data)
233
- return nil unless periods_data && periods_data.is_a?(Array)
233
+ return nil unless periods_data.is_a?(Array)
234
234
 
235
235
  periods_data.map { |period_data| SessionPeriod.new(period_data) }
236
236
  end
@@ -11,6 +11,7 @@ module SchwabRb
11
11
  end
12
12
 
13
13
  class MarketMovers
14
+ include Enumerable
14
15
  attr_reader :movers
15
16
 
16
17
  def initialize(movers)
@@ -123,7 +123,7 @@ module SchwabRb
123
123
  end
124
124
 
125
125
  def expires_today?
126
- @days_to_expiration == 0
126
+ @days_to_expiration.zero?
127
127
  end
128
128
 
129
129
  def expires_tomorrow?
@@ -42,13 +42,13 @@ module SchwabRb
42
42
  def commission
43
43
  return 0.0 unless @projected_commission
44
44
 
45
- @projected_commission.commission.to_f
45
+ @projected_commission.commission_value
46
46
  end
47
47
 
48
48
  def fees
49
49
  return 0.0 unless @projected_commission
50
50
 
51
- @projected_commission.fee.to_f
51
+ @projected_commission.fee_value
52
52
  end
53
53
 
54
54
  def to_h
@@ -145,71 +145,43 @@ module SchwabRb
145
145
  end
146
146
 
147
147
  class CommissionAndFee
148
- attr_reader :commission_data, :fee_data
148
+ attr_reader :commission, :commissions, :fee, :fees, :true_commission
149
149
 
150
150
  def initialize(attrs)
151
- # Handle the flattened structure (from API)
152
- if attrs[:commissions]
153
- @commission_data = attrs[:commissions]
154
- @fee_data = attrs[:fees] || []
155
- @true_commission_data = []
156
- @direct_commission = attrs[:commission]
157
- @direct_fee = attrs[:fee]
158
- @direct_true_commission = attrs[:trueCommission]
159
- # Handle the nested structure (from test data)
160
- else
161
- @commission_data = attrs.dig(:commission, :commissionLegs) || []
162
- @fee_data = attrs.dig(:fee, :feeLegs) || []
163
- @true_commission_data = attrs.dig(:trueCommission, :commissionLegs) || []
164
- @direct_commission = nil
165
- @direct_fee = nil
166
- @direct_true_commission = nil
167
- end
151
+ @commission = attrs[:commission]&.to_f
152
+ @commissions = attrs[:commissions] || []
153
+ @fee = attrs[:fee]&.to_f
154
+ @fees = attrs[:fees] || []
155
+ @true_commission = attrs[:trueCommission]&.to_f
168
156
  end
169
157
 
170
158
  def commission_total
171
- calculate_total_from_legs(@commission_data, "COMMISSION")
159
+ calculate_total_from_legs(@commissions, "COMMISSION")
172
160
  end
173
161
 
174
- def commission
175
- @direct_commission || format("%.2f", commission_total)
162
+ def commission_value
163
+ @commission || commission_total
176
164
  end
177
165
 
178
166
  def fee_total
179
- calculate_total_from_legs(@fee_data, %w[OPT_REG_FEE INDEX_OPTION_FEE])
180
- end
181
-
182
- def fee
183
- @direct_fee || format("%.2f", fee_total)
184
- end
185
-
186
- def true_commission
187
- if @direct_true_commission
188
- @direct_true_commission
189
- elsif @true_commission_data.any?
190
- # For nested structure, calculate from true commission legs
191
- true_commission_total = calculate_total_from_legs(@true_commission_data, "COMMISSION")
192
- format("%.2f", true_commission_total * 2)
193
- else
194
- format("%.2f", commission_total * 2)
195
- end
167
+ calculate_total_from_legs(@fees, %w[OPT_REG_FEE INDEX_OPTION_FEE])
196
168
  end
197
169
 
198
- def commissions
199
- @commission_data
170
+ def fee_value
171
+ @fee || fee_total
200
172
  end
201
173
 
202
- def fees
203
- @fee_data
174
+ def true_commission_value
175
+ @true_commission || (commission_total * 2)
204
176
  end
205
177
 
206
178
  def to_h
207
179
  {
208
- commission: commission,
209
- fee: fee,
210
- trueCommission: true_commission,
211
- commissions: commissions,
212
- fees: fees
180
+ commission: @commission,
181
+ fee: @fee,
182
+ trueCommission: @true_commission,
183
+ commissions: @commissions,
184
+ fees: @fees
213
185
  }
214
186
  end
215
187
 
@@ -137,7 +137,7 @@ module SchwabRb
137
137
  end
138
138
 
139
139
  def price_change_percent
140
- return 0 if @open == 0
140
+ return 0 if @open.zero?
141
141
 
142
142
  ((price_change / @open) * 100).round(4)
143
143
  end
@@ -105,6 +105,14 @@ module SchwabRb
105
105
  type == "TRADE"
106
106
  end
107
107
 
108
+ def fees
109
+ transfer_items.select(&:fee?).map { |ti| ti.amount }
110
+ end
111
+
112
+ def commissions
113
+ transfer_items.select(&:commission?).map { |ti| ti.amount }
114
+ end
115
+
108
116
  def symbols
109
117
  transfer_items.map { |ti| ti.instrument.symbol }
110
118
  end