schwab_rb 0.3.12 → 0.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -0
- data/doc/ACCOUNT_MANAGEMENT.md +297 -0
- data/doc/PLACE_ORDER_SAMPLES.md +301 -0
- data/doc/QUICK_START.md +239 -0
- data/examples/place_oco_order.rb +75 -0
- data/lib/schwab_rb/account_hash_manager.rb +119 -0
- data/lib/schwab_rb/clients/base_client.rb +231 -115
- data/lib/schwab_rb/configuration.rb +7 -1
- data/lib/schwab_rb/orders/iron_condor_order.rb +14 -26
- data/lib/schwab_rb/orders/oco_order.rb +69 -0
- data/lib/schwab_rb/orders/order.rb +45 -2
- data/lib/schwab_rb/orders/order_factory.rb +21 -10
- data/lib/schwab_rb/orders/single_order.rb +16 -7
- data/lib/schwab_rb/orders/vertical_order.rb +23 -26
- data/lib/schwab_rb/version.rb +1 -1
- data/lib/schwab_rb.rb +2 -0
- metadata +8 -2
data/doc/QUICK_START.md
ADDED
@@ -0,0 +1,239 @@
|
|
1
|
+
# Quick Start Guide
|
2
|
+
|
3
|
+
Get started with schwab_rb in 5 minutes.
|
4
|
+
|
5
|
+
## 1. Install
|
6
|
+
|
7
|
+
```bash
|
8
|
+
gem install schwab_rb
|
9
|
+
```
|
10
|
+
|
11
|
+
Or add to your Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'schwab_rb'
|
15
|
+
```
|
16
|
+
|
17
|
+
## 2. Get Your Schwab API Credentials
|
18
|
+
|
19
|
+
1. Go to [Schwab Developer Portal](https://developer.schwab.com/)
|
20
|
+
2. Create an app to get:
|
21
|
+
- **API Key** (App Key)
|
22
|
+
- **App Secret** (Secret)
|
23
|
+
- **Callback URL** (use `https://127.0.0.1:8182` for local development)
|
24
|
+
|
25
|
+
## 3. Set Up Environment Variables
|
26
|
+
|
27
|
+
Create a `.env` file:
|
28
|
+
|
29
|
+
```bash
|
30
|
+
SCHWAB_API_KEY=your_api_key_here
|
31
|
+
SCHWAB_APP_SECRET=your_app_secret_here
|
32
|
+
SCHWAB_APP_CALLBACK_URL=https://127.0.0.1:8182
|
33
|
+
SCHWAB_TOKEN_PATH=~/.schwab_rb/token.json
|
34
|
+
```
|
35
|
+
|
36
|
+
## 4. Initialize Client (First Time)
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
require 'schwab_rb'
|
40
|
+
require 'dotenv/load'
|
41
|
+
|
42
|
+
# This will open your browser for authentication
|
43
|
+
client = SchwabRb::Auth.init_client_easy(
|
44
|
+
ENV['SCHWAB_API_KEY'],
|
45
|
+
ENV['SCHWAB_APP_SECRET'],
|
46
|
+
ENV['SCHWAB_APP_CALLBACK_URL'],
|
47
|
+
ENV['SCHWAB_TOKEN_PATH']
|
48
|
+
)
|
49
|
+
|
50
|
+
puts "✓ Authenticated successfully!"
|
51
|
+
```
|
52
|
+
|
53
|
+
**What happens:**
|
54
|
+
- Browser opens to Schwab login
|
55
|
+
- You log in and authorize
|
56
|
+
- Token saved to `~/.schwab_rb/token.json`
|
57
|
+
- Client ready to use!
|
58
|
+
|
59
|
+
## 5. Make Your First API Call
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
# Get your accounts
|
63
|
+
accounts = client.get_accounts
|
64
|
+
|
65
|
+
accounts.each do |account|
|
66
|
+
puts "Account: #{account.account_number}"
|
67
|
+
puts "Type: #{account.type}"
|
68
|
+
puts "Value: $#{account.balance.total_value}"
|
69
|
+
puts "---"
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
## 6. Set Up Account Names (Optional but Recommended)
|
74
|
+
|
75
|
+
Create `~/.schwab_rb/account_names.json`:
|
76
|
+
|
77
|
+
```json
|
78
|
+
{
|
79
|
+
"my_trading": "12345678",
|
80
|
+
"my_ira": "87654321"
|
81
|
+
}
|
82
|
+
```
|
83
|
+
|
84
|
+
Populate account hashes:
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
client.get_account_numbers
|
88
|
+
```
|
89
|
+
|
90
|
+
Now use friendly names:
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
# Instead of:
|
94
|
+
# account = client.get_account("ABC123HASH")
|
95
|
+
|
96
|
+
# Do this:
|
97
|
+
account = client.get_account(account_name: "my_trading")
|
98
|
+
```
|
99
|
+
|
100
|
+
## Common Operations
|
101
|
+
|
102
|
+
### Get Account Info
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
account = client.get_account(account_name: "my_trading")
|
106
|
+
puts "Buying Power: $#{account.balance.buying_power}"
|
107
|
+
```
|
108
|
+
|
109
|
+
### Get Quotes
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
quote = client.get_quote("AAPL")
|
113
|
+
puts "#{quote.symbol}: $#{quote.last_price}"
|
114
|
+
```
|
115
|
+
|
116
|
+
### Get Price History
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
history = client.get_price_history_every_day(
|
120
|
+
"AAPL",
|
121
|
+
start_datetime: DateTime.now - 30
|
122
|
+
)
|
123
|
+
|
124
|
+
puts "#{history.candles.size} days of data"
|
125
|
+
history.candles.last(5).each do |candle|
|
126
|
+
puts "#{candle.date}: $#{candle.close}"
|
127
|
+
end
|
128
|
+
```
|
129
|
+
|
130
|
+
### Place Order (Market Buy)
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
order = SchwabRb::Orders::Builder.new
|
134
|
+
.set_session(:normal)
|
135
|
+
.set_duration(:day)
|
136
|
+
.set_order_type(:market)
|
137
|
+
.add_equity_leg(:buy, 'AAPL', 10)
|
138
|
+
.build
|
139
|
+
|
140
|
+
response = client.place_order(order, account_name: "my_trading")
|
141
|
+
puts "Order placed!"
|
142
|
+
```
|
143
|
+
|
144
|
+
### Get Orders
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
orders = client.get_account_orders(account_name: "my_trading")
|
148
|
+
|
149
|
+
orders.each do |order|
|
150
|
+
puts "#{order.status}: #{order.order_leg_collection.first.quantity} shares of #{order.order_leg_collection.first.instrument.symbol}"
|
151
|
+
end
|
152
|
+
```
|
153
|
+
|
154
|
+
### Get Transactions
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
transactions = client.get_transactions(
|
158
|
+
account_name: "my_trading",
|
159
|
+
start_date: DateTime.now - 7
|
160
|
+
)
|
161
|
+
|
162
|
+
puts "#{transactions.size} transactions in the last week"
|
163
|
+
```
|
164
|
+
|
165
|
+
## Configuration (Optional)
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
SchwabRb.configure do |config|
|
169
|
+
# Logging
|
170
|
+
config.log_level = "INFO"
|
171
|
+
config.log_file = "schwab.log"
|
172
|
+
|
173
|
+
# Account management
|
174
|
+
config.schwab_home = "~/.schwab_rb"
|
175
|
+
config.account_names_path = "~/.schwab_rb/account_names.json"
|
176
|
+
config.account_hashes_path = "~/.schwab_rb/account_hashes.json"
|
177
|
+
end
|
178
|
+
```
|
179
|
+
|
180
|
+
## Next Steps
|
181
|
+
|
182
|
+
- **Trading**: See [examples/](../examples/) for order placement examples
|
183
|
+
- **Account Management**: Read [ACCOUNT_MANAGEMENT.md](./ACCOUNT_MANAGEMENT.md) for multi-account setup
|
184
|
+
- **API Reference**: Check the [main README](../README.md) for complete API documentation
|
185
|
+
|
186
|
+
## Troubleshooting
|
187
|
+
|
188
|
+
**Authentication fails?**
|
189
|
+
- Check your API credentials
|
190
|
+
- Make sure callback URL matches exactly
|
191
|
+
- Try deleting `~/.schwab_rb/token.json` and re-authenticating
|
192
|
+
|
193
|
+
**Token expired?**
|
194
|
+
- The client automatically refreshes tokens
|
195
|
+
- If issues persist, re-authenticate with `init_client_easy`
|
196
|
+
|
197
|
+
**Can't find account?**
|
198
|
+
- Run `client.get_account_numbers` to refresh account hashes
|
199
|
+
- Check `account_names.json` for typos
|
200
|
+
- Use `client.available_account_names` to see configured accounts
|
201
|
+
|
202
|
+
## Example Script
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
#!/usr/bin/env ruby
|
206
|
+
require 'schwab_rb'
|
207
|
+
require 'dotenv/load'
|
208
|
+
|
209
|
+
# Initialize
|
210
|
+
client = SchwabRb::Auth.init_client_easy(
|
211
|
+
ENV['SCHWAB_API_KEY'],
|
212
|
+
ENV['SCHWAB_APP_SECRET'],
|
213
|
+
ENV['SCHWAB_APP_CALLBACK_URL'],
|
214
|
+
ENV['SCHWAB_TOKEN_PATH']
|
215
|
+
)
|
216
|
+
|
217
|
+
# Get available accounts
|
218
|
+
names = client.available_account_names
|
219
|
+
puts "Available accounts: #{names.join(', ')}"
|
220
|
+
|
221
|
+
# Check account balance
|
222
|
+
account = client.get_account(account_name: names.first)
|
223
|
+
puts "\n#{names.first}:"
|
224
|
+
puts " Total Value: $#{account.balance.total_value}"
|
225
|
+
puts " Cash: $#{account.balance.cash_balance}"
|
226
|
+
puts " Buying Power: $#{account.balance.buying_power}"
|
227
|
+
|
228
|
+
# Get a quote
|
229
|
+
quote = client.get_quote("SPY")
|
230
|
+
puts "\nSPY: $#{quote.last_price}"
|
231
|
+
|
232
|
+
puts "\n✓ All done!"
|
233
|
+
```
|
234
|
+
|
235
|
+
---
|
236
|
+
|
237
|
+
**Need Help?**
|
238
|
+
- Check the [main README](../README.md)
|
239
|
+
- Review [examples/](../examples/)
|
@@ -0,0 +1,75 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'schwab_rb'
|
6
|
+
require 'dotenv'
|
7
|
+
require 'pry'
|
8
|
+
|
9
|
+
Dotenv.load
|
10
|
+
|
11
|
+
# Example: Place an OCO (One Cancels Another) order
|
12
|
+
#
|
13
|
+
# This example demonstrates how to create an OCO order where two orders
|
14
|
+
# are submitted simultaneously, and if one fills, the other is automatically cancelled.
|
15
|
+
#
|
16
|
+
|
17
|
+
# SchwabRb::Configuration.configure do |config|
|
18
|
+
# end
|
19
|
+
|
20
|
+
CURRENT_ACCT = "TRADING_BROKERAGE_ACCOUNT"
|
21
|
+
acct_manager = SchwabRb::AccountHashManager.new
|
22
|
+
|
23
|
+
client = SchwabRb::Auth.init_client_easy(
|
24
|
+
ENV['SCHWAB_API_KEY'],
|
25
|
+
ENV['SCHWAB_APP_SECRET'],
|
26
|
+
ENV['SCHWAB_APP_CALLBACK_URL'],
|
27
|
+
ENV['SCHWAB_TOKEN_PATH']
|
28
|
+
)
|
29
|
+
|
30
|
+
puts "Example 1: OCO order with take profit and stop loss"
|
31
|
+
puts "=" * 60
|
32
|
+
|
33
|
+
|
34
|
+
symbols = [
|
35
|
+
"SPXW 251020P06510000", # long put
|
36
|
+
"SPXW 251020P06530000", # short put
|
37
|
+
"SPXW 251020C06790000", # long call
|
38
|
+
"SPXW 251020C06770000", # short call
|
39
|
+
]
|
40
|
+
|
41
|
+
oco_order = SchwabRb::Orders::OrderFactory.build(
|
42
|
+
strategy_type: SchwabRb::Order::OrderStrategyTypes::OCO,
|
43
|
+
child_order_specs: [
|
44
|
+
{
|
45
|
+
strategy_type: SchwabRb::Order::ComplexOrderStrategyTypes::VERTICAL,
|
46
|
+
short_leg_symbol: "SPXW 251020P06530000",
|
47
|
+
long_leg_symbol: "SPXW 251020P06510000",
|
48
|
+
order_type: SchwabRb::Order::Types::STOP_LIMIT,
|
49
|
+
price: 2.1,
|
50
|
+
stop_price: 2.0,
|
51
|
+
order_instruction: :close,
|
52
|
+
credit_debit: :debit,
|
53
|
+
quantity: 2
|
54
|
+
},
|
55
|
+
{
|
56
|
+
strategy_type: SchwabRb::Order::ComplexOrderStrategyTypes::VERTICAL,
|
57
|
+
short_leg_symbol: "SPXW 251020C06770000",
|
58
|
+
long_leg_symbol: "SPXW 251020C06790000",
|
59
|
+
order_type: SchwabRb::Order::Types::STOP_LIMIT,
|
60
|
+
price: 2.1,
|
61
|
+
stop_price: 2.0,
|
62
|
+
order_instruction: :close,
|
63
|
+
credit_debit: :debit,
|
64
|
+
quantity: 2
|
65
|
+
}
|
66
|
+
]
|
67
|
+
)
|
68
|
+
|
69
|
+
built_order = oco_order.build
|
70
|
+
|
71
|
+
binding.pry
|
72
|
+
|
73
|
+
response = client.place_order(built_order, account_name: CURRENT_ACCT)
|
74
|
+
|
75
|
+
binding.pry
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "fileutils"
|
5
|
+
|
6
|
+
module SchwabRb
|
7
|
+
class AccountHashManager
|
8
|
+
class AccountNamesFileNotFoundError < StandardError; end
|
9
|
+
class InvalidAccountNamesFileError < StandardError; end
|
10
|
+
|
11
|
+
attr_reader :account_names_path, :account_hashes_path, :account_names
|
12
|
+
|
13
|
+
def initialize(account_names_path = nil, account_hashes_path = nil)
|
14
|
+
@account_names_path = account_names_path || SchwabRb.configuration.account_names_path
|
15
|
+
@account_hashes_path = account_hashes_path || SchwabRb.configuration.account_hashes_path
|
16
|
+
@account_names_path = File.expand_path(@account_names_path)
|
17
|
+
@account_hashes_path = File.expand_path(@account_hashes_path)
|
18
|
+
@account_names = []
|
19
|
+
end
|
20
|
+
|
21
|
+
def update_hashes_from_api_response(account_numbers_response)
|
22
|
+
account_names = load_account_names
|
23
|
+
current_hashes = load_account_hashes
|
24
|
+
|
25
|
+
number_to_hash = {}
|
26
|
+
account_numbers_response.each do |account_data|
|
27
|
+
account_number = account_data[:accountNumber]
|
28
|
+
hash_value = account_data[:hashValue]
|
29
|
+
number_to_hash[account_number] = hash_value
|
30
|
+
end
|
31
|
+
|
32
|
+
updated_hashes = {}
|
33
|
+
missing_accounts = []
|
34
|
+
|
35
|
+
account_names.each do |name, account_number|
|
36
|
+
if number_to_hash.key?(account_number)
|
37
|
+
updated_hashes[name] = number_to_hash[account_number]
|
38
|
+
elsif current_hashes.key?(name)
|
39
|
+
# Keep existing hash but warn that account wasn't in API response
|
40
|
+
updated_hashes[name] = current_hashes[name]
|
41
|
+
missing_accounts << { name: name, number: account_number }
|
42
|
+
else
|
43
|
+
# Account name exists but no hash found (new or invalid account)
|
44
|
+
missing_accounts << { name: name, number: account_number }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Log warnings for accounts that weren't found in API response
|
49
|
+
if missing_accounts.any?
|
50
|
+
missing_accounts.each do |account|
|
51
|
+
SchwabRb::Logger.logger.warn(
|
52
|
+
"Account '#{account[:name]}' (#{account[:number]}) not found in API response. " \
|
53
|
+
"This may indicate a closed account or incorrect account number in account_names.json"
|
54
|
+
)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
save_account_hashes(updated_hashes)
|
59
|
+
updated_hashes
|
60
|
+
end
|
61
|
+
|
62
|
+
def get_hash_by_name(account_name)
|
63
|
+
hashes = load_account_hashes
|
64
|
+
hashes[account_name]
|
65
|
+
end
|
66
|
+
|
67
|
+
def get_all_hashes
|
68
|
+
load_account_hashes
|
69
|
+
end
|
70
|
+
|
71
|
+
def available_account_names
|
72
|
+
begin
|
73
|
+
load_account_names.keys
|
74
|
+
rescue AccountNamesFileNotFoundError
|
75
|
+
[]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def load_account_hashes
|
82
|
+
return {} unless File.exist?(@account_hashes_path)
|
83
|
+
|
84
|
+
begin
|
85
|
+
json_content = File.read(@account_hashes_path)
|
86
|
+
JSON.parse(json_content)
|
87
|
+
rescue JSON::ParserError
|
88
|
+
{}
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def save_account_hashes(hashes_map)
|
93
|
+
FileUtils.mkdir_p(File.dirname(@account_hashes_path))
|
94
|
+
|
95
|
+
File.write(@account_hashes_path, JSON.pretty_generate(hashes_map))
|
96
|
+
end
|
97
|
+
|
98
|
+
def load_account_names
|
99
|
+
unless File.exist?(@account_names_path)
|
100
|
+
raise AccountNamesFileNotFoundError,
|
101
|
+
"Account names file not found at #{@account_names_path}. " \
|
102
|
+
"Please create a JSON file mapping account names to account numbers. " \
|
103
|
+
"Example: {\"my_trading_account\": \"12345678\", \"my_ira\": \"87654321\"}"
|
104
|
+
end
|
105
|
+
|
106
|
+
begin
|
107
|
+
json_content = File.read(@account_names_path)
|
108
|
+
return {} if json_content.strip.empty?
|
109
|
+
|
110
|
+
account_names_hash = JSON.parse(json_content)
|
111
|
+
@account_names = account_names_hash.keys
|
112
|
+
account_names_hash
|
113
|
+
rescue JSON::ParserError => e
|
114
|
+
raise InvalidAccountNamesFileError,
|
115
|
+
"Invalid JSON in account names file at #{@account_names_path}: #{e.message}"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|