tastytrade 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.
- checksums.yaml +7 -0
- data/.claude/commands/release-pr.md +108 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +29 -0
- data/.github/ISSUE_TEMPLATE/roadmap_task.md +34 -0
- data/.github/dependabot.yml +11 -0
- data/.github/workflows/main.yml +75 -0
- data/.rspec +3 -0
- data/.rubocop.yml +101 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +100 -0
- data/CLAUDE.md +78 -0
- data/CODE_OF_CONDUCT.md +81 -0
- data/CONTRIBUTING.md +89 -0
- data/DISCLAIMER.md +54 -0
- data/LICENSE.txt +24 -0
- data/README.md +235 -0
- data/ROADMAP.md +157 -0
- data/Rakefile +17 -0
- data/SECURITY.md +48 -0
- data/docs/getting_started.md +48 -0
- data/docs/python_sdk_analysis.md +181 -0
- data/exe/tastytrade +8 -0
- data/lib/tastytrade/cli.rb +604 -0
- data/lib/tastytrade/cli_config.rb +79 -0
- data/lib/tastytrade/cli_helpers.rb +178 -0
- data/lib/tastytrade/client.rb +117 -0
- data/lib/tastytrade/keyring_store.rb +72 -0
- data/lib/tastytrade/models/account.rb +129 -0
- data/lib/tastytrade/models/account_balance.rb +75 -0
- data/lib/tastytrade/models/base.rb +47 -0
- data/lib/tastytrade/models/current_position.rb +155 -0
- data/lib/tastytrade/models/user.rb +23 -0
- data/lib/tastytrade/models.rb +7 -0
- data/lib/tastytrade/session.rb +164 -0
- data/lib/tastytrade/session_manager.rb +160 -0
- data/lib/tastytrade/version.rb +5 -0
- data/lib/tastytrade.rb +31 -0
- data/sig/tastytrade.rbs +4 -0
- data/spec/exe/tastytrade_spec.rb +104 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/tastytrade/cli_accounts_spec.rb +166 -0
- data/spec/tastytrade/cli_auth_spec.rb +216 -0
- data/spec/tastytrade/cli_config_spec.rb +180 -0
- data/spec/tastytrade/cli_helpers_spec.rb +248 -0
- data/spec/tastytrade/cli_interactive_spec.rb +54 -0
- data/spec/tastytrade/cli_logout_spec.rb +121 -0
- data/spec/tastytrade/cli_select_spec.rb +174 -0
- data/spec/tastytrade/cli_status_spec.rb +206 -0
- data/spec/tastytrade/client_spec.rb +210 -0
- data/spec/tastytrade/keyring_store_spec.rb +168 -0
- data/spec/tastytrade/models/account_balance_spec.rb +247 -0
- data/spec/tastytrade/models/account_spec.rb +206 -0
- data/spec/tastytrade/models/base_spec.rb +61 -0
- data/spec/tastytrade/models/current_position_spec.rb +444 -0
- data/spec/tastytrade/models/user_spec.rb +58 -0
- data/spec/tastytrade/session_manager_spec.rb +296 -0
- data/spec/tastytrade/session_spec.rb +392 -0
- data/spec/tastytrade_spec.rb +9 -0
- metadata +303 -0
@@ -0,0 +1,155 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bigdecimal"
|
4
|
+
|
5
|
+
module Tastytrade
|
6
|
+
module Models
|
7
|
+
# Represents a current position in an account
|
8
|
+
class CurrentPosition < Base
|
9
|
+
attr_reader :account_number, :symbol, :instrument_type, :underlying_symbol,
|
10
|
+
:quantity, :quantity_direction, :close_price, :average_open_price,
|
11
|
+
:average_yearly_market_close_price, :average_daily_market_close_price,
|
12
|
+
:multiplier, :cost_effect, :is_suppressed, :is_frozen,
|
13
|
+
:realized_day_gain, :realized_today, :created_at, :updated_at,
|
14
|
+
:mark, :mark_price, :restricted_quantity, :expires_at,
|
15
|
+
:root_symbol, :option_expiration_type, :strike_price,
|
16
|
+
:option_type, :contract_size, :exercise_style
|
17
|
+
|
18
|
+
def initialize(data)
|
19
|
+
super
|
20
|
+
@account_number = data["account-number"]
|
21
|
+
@symbol = data["symbol"]
|
22
|
+
@instrument_type = data["instrument-type"]
|
23
|
+
@underlying_symbol = data["underlying-symbol"]
|
24
|
+
|
25
|
+
# Quantity information
|
26
|
+
@quantity = parse_decimal(data["quantity"])
|
27
|
+
@quantity_direction = data["quantity-direction"]
|
28
|
+
@restricted_quantity = parse_decimal(data["restricted-quantity"])
|
29
|
+
|
30
|
+
# Price information
|
31
|
+
@close_price = parse_decimal(data["close-price"])
|
32
|
+
@average_open_price = parse_decimal(data["average-open-price"])
|
33
|
+
@average_yearly_market_close_price = parse_decimal(data["average-yearly-market-close-price"])
|
34
|
+
@average_daily_market_close_price = parse_decimal(data["average-daily-market-close-price"])
|
35
|
+
@mark = parse_decimal(data["mark"])
|
36
|
+
@mark_price = parse_decimal(data["mark-price"])
|
37
|
+
|
38
|
+
# Position details
|
39
|
+
@multiplier = data["multiplier"]&.to_i || 1
|
40
|
+
@cost_effect = data["cost-effect"]
|
41
|
+
@is_suppressed = data["is-suppressed"] || false
|
42
|
+
@is_frozen = data["is-frozen"] || false
|
43
|
+
|
44
|
+
# Realized gains
|
45
|
+
@realized_day_gain = parse_decimal(data["realized-day-gain"])
|
46
|
+
@realized_today = parse_decimal(data["realized-today"])
|
47
|
+
|
48
|
+
# Timestamps
|
49
|
+
@created_at = parse_time(data["created-at"])
|
50
|
+
@updated_at = parse_time(data["updated-at"])
|
51
|
+
@expires_at = parse_time(data["expires-at"])
|
52
|
+
|
53
|
+
# Option-specific fields
|
54
|
+
@root_symbol = data["root-symbol"]
|
55
|
+
@option_expiration_type = data["option-expiration-type"]
|
56
|
+
@strike_price = parse_decimal(data["strike-price"])
|
57
|
+
@option_type = data["option-type"]
|
58
|
+
@contract_size = data["contract-size"]&.to_i
|
59
|
+
@exercise_style = data["exercise-style"]
|
60
|
+
end
|
61
|
+
|
62
|
+
# Check if this is a long position
|
63
|
+
def long?
|
64
|
+
quantity_direction == "Long"
|
65
|
+
end
|
66
|
+
|
67
|
+
# Check if this is a short position
|
68
|
+
def short?
|
69
|
+
quantity_direction == "Short"
|
70
|
+
end
|
71
|
+
|
72
|
+
# Check if position is closed (zero quantity)
|
73
|
+
def closed?
|
74
|
+
quantity_direction == "Zero" || quantity.zero?
|
75
|
+
end
|
76
|
+
|
77
|
+
# Check if this is an equity position
|
78
|
+
def equity?
|
79
|
+
instrument_type == "Equity"
|
80
|
+
end
|
81
|
+
|
82
|
+
# Check if this is an option position
|
83
|
+
def option?
|
84
|
+
instrument_type == "Equity Option"
|
85
|
+
end
|
86
|
+
|
87
|
+
# Check if this is a futures position
|
88
|
+
def futures?
|
89
|
+
instrument_type == "Future"
|
90
|
+
end
|
91
|
+
|
92
|
+
# Check if this is a futures option position
|
93
|
+
def futures_option?
|
94
|
+
instrument_type == "Future Option"
|
95
|
+
end
|
96
|
+
|
97
|
+
# Calculate position value (quantity * price * multiplier)
|
98
|
+
def position_value
|
99
|
+
return BigDecimal("0") if closed?
|
100
|
+
price = mark_price.zero? ? close_price : mark_price
|
101
|
+
quantity.abs * price * multiplier
|
102
|
+
end
|
103
|
+
|
104
|
+
# Calculate unrealized P&L
|
105
|
+
def unrealized_pnl
|
106
|
+
return BigDecimal("0") if closed? || average_open_price.zero?
|
107
|
+
|
108
|
+
current_price = mark_price.zero? ? close_price : mark_price
|
109
|
+
if long?
|
110
|
+
(current_price - average_open_price) * quantity * multiplier
|
111
|
+
else
|
112
|
+
(average_open_price - current_price) * quantity.abs * multiplier
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Calculate unrealized P&L percentage
|
117
|
+
def unrealized_pnl_percentage
|
118
|
+
return BigDecimal("0") if closed? || average_open_price.zero?
|
119
|
+
|
120
|
+
cost_basis = average_open_price * quantity.abs * multiplier
|
121
|
+
return BigDecimal("0") if cost_basis.zero?
|
122
|
+
|
123
|
+
(unrealized_pnl / cost_basis * 100).round(2)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Calculate total P&L (realized + unrealized)
|
127
|
+
def total_pnl
|
128
|
+
realized_today + unrealized_pnl
|
129
|
+
end
|
130
|
+
|
131
|
+
# Get display symbol (simplified for options)
|
132
|
+
def display_symbol
|
133
|
+
if option?
|
134
|
+
# Format: ROOT MM/DD/YY C/P STRIKE
|
135
|
+
return symbol unless expires_at && strike_price && option_type
|
136
|
+
|
137
|
+
exp_date = expires_at.strftime("%m/%d/%y")
|
138
|
+
type_char = option_type == "Call" ? "C" : "P"
|
139
|
+
strike_str = strike_price.to_s("F")
|
140
|
+
"#{root_symbol} #{exp_date} #{type_char} #{strike_str}"
|
141
|
+
else
|
142
|
+
symbol
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
# Parse string value to BigDecimal, handling nil and empty strings
|
149
|
+
def parse_decimal(value)
|
150
|
+
return BigDecimal("0") if value.nil? || value.to_s.empty?
|
151
|
+
BigDecimal(value.to_s)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tastytrade
|
4
|
+
module Models
|
5
|
+
# Represents a Tastytrade user
|
6
|
+
class User < Base
|
7
|
+
attr_reader :email, :username, :external_id, :is_professional
|
8
|
+
|
9
|
+
def professional?
|
10
|
+
@is_professional == true
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def parse_attributes
|
16
|
+
@email = @data["email"]
|
17
|
+
@username = @data["username"]
|
18
|
+
@external_id = @data["external-id"]
|
19
|
+
@is_professional = @data["is-professional"]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "models"
|
4
|
+
|
5
|
+
module Tastytrade
|
6
|
+
# Manages authentication and session state for Tastytrade API
|
7
|
+
class Session
|
8
|
+
attr_reader :user, :session_token, :remember_token, :is_test, :session_expiration
|
9
|
+
|
10
|
+
# Initialize a new session
|
11
|
+
#
|
12
|
+
# @param username [String] Tastytrade username
|
13
|
+
# @param password [String] Tastytrade password (optional if remember_token provided)
|
14
|
+
# @param remember_me [Boolean] Whether to save remember token
|
15
|
+
# @param remember_token [String] Existing remember token for re-authentication
|
16
|
+
# @param is_test [Boolean] Use test environment
|
17
|
+
def initialize(username:, password: nil, remember_me: false, remember_token: nil, is_test: false, timeout: Client::DEFAULT_TIMEOUT)
|
18
|
+
@username = username
|
19
|
+
@password = password
|
20
|
+
@remember_me = remember_me
|
21
|
+
@remember_token = remember_token
|
22
|
+
@is_test = is_test
|
23
|
+
@client = Client.new(base_url: api_url, timeout: timeout)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Authenticate with Tastytrade API
|
27
|
+
#
|
28
|
+
# @return [Session] Self for method chaining
|
29
|
+
# @raise [Tastytrade::Error] If authentication fails
|
30
|
+
def login
|
31
|
+
response = @client.post("/sessions", login_credentials)
|
32
|
+
data = response["data"]
|
33
|
+
|
34
|
+
@user = Models::User.new(data["user"])
|
35
|
+
@session_token = data["session-token"]
|
36
|
+
@remember_token = data["remember-token"] if @remember_me
|
37
|
+
|
38
|
+
# Track session expiration if provided
|
39
|
+
if data["session-expiration"]
|
40
|
+
@session_expiration = Time.parse(data["session-expiration"])
|
41
|
+
end
|
42
|
+
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
# Validate current session
|
47
|
+
#
|
48
|
+
# @return [Boolean] True if session is valid
|
49
|
+
def validate
|
50
|
+
response = get("/sessions/validate")
|
51
|
+
response["data"]["email"] == @user.email
|
52
|
+
rescue Tastytrade::Error
|
53
|
+
false
|
54
|
+
end
|
55
|
+
|
56
|
+
# Destroy current session
|
57
|
+
#
|
58
|
+
# @return [nil]
|
59
|
+
def destroy
|
60
|
+
delete("/sessions") if @session_token
|
61
|
+
@session_token = nil
|
62
|
+
@remember_token = nil
|
63
|
+
@user = nil
|
64
|
+
end
|
65
|
+
|
66
|
+
# Make authenticated GET request
|
67
|
+
#
|
68
|
+
# @param path [String] API endpoint path
|
69
|
+
# @param params [Hash] Query parameters
|
70
|
+
# @return [Hash] Parsed response
|
71
|
+
def get(path, params = {})
|
72
|
+
@client.get(path, params, auth_headers)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Make authenticated POST request
|
76
|
+
#
|
77
|
+
# @param path [String] API endpoint path
|
78
|
+
# @param body [Hash] Request body
|
79
|
+
# @return [Hash] Parsed response
|
80
|
+
def post(path, body = {})
|
81
|
+
@client.post(path, body, auth_headers)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Make authenticated PUT request
|
85
|
+
#
|
86
|
+
# @param path [String] API endpoint path
|
87
|
+
# @param body [Hash] Request body
|
88
|
+
# @return [Hash] Parsed response
|
89
|
+
def put(path, body = {})
|
90
|
+
@client.put(path, body, auth_headers)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Make authenticated DELETE request
|
94
|
+
#
|
95
|
+
# @param path [String] API endpoint path
|
96
|
+
# @return [Hash] Parsed response
|
97
|
+
def delete(path)
|
98
|
+
@client.delete(path, auth_headers)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Check if authenticated
|
102
|
+
#
|
103
|
+
# @return [Boolean] True if session has token
|
104
|
+
def authenticated?
|
105
|
+
!@session_token.nil?
|
106
|
+
end
|
107
|
+
|
108
|
+
# Check if session is expired
|
109
|
+
#
|
110
|
+
# @return [Boolean] True if session is expired
|
111
|
+
def expired?
|
112
|
+
return false unless @session_expiration
|
113
|
+
Time.now >= @session_expiration
|
114
|
+
end
|
115
|
+
|
116
|
+
# Time remaining until session expires
|
117
|
+
#
|
118
|
+
# @return [Float, nil] Seconds until expiration
|
119
|
+
def time_until_expiry
|
120
|
+
return nil unless @session_expiration
|
121
|
+
@session_expiration - Time.now
|
122
|
+
end
|
123
|
+
|
124
|
+
# Refresh session using remember token
|
125
|
+
#
|
126
|
+
# @return [Session] Self
|
127
|
+
# @raise [Tastytrade::Error] If refresh fails
|
128
|
+
def refresh_session
|
129
|
+
raise Tastytrade::Error, "No remember token available" unless @remember_token
|
130
|
+
|
131
|
+
# Clear password and re-login with remember token
|
132
|
+
@password = nil
|
133
|
+
login
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
def api_url
|
139
|
+
@is_test ? Tastytrade::CERT_URL : Tastytrade::API_URL
|
140
|
+
end
|
141
|
+
|
142
|
+
def auth_headers
|
143
|
+
raise Tastytrade::Error, "Not authenticated" unless @session_token
|
144
|
+
|
145
|
+
{ "Authorization" => @session_token }
|
146
|
+
end
|
147
|
+
|
148
|
+
def login_credentials
|
149
|
+
credentials = {
|
150
|
+
"login" => @username,
|
151
|
+
"remember-me" => @remember_me
|
152
|
+
}
|
153
|
+
|
154
|
+
# Use remember token if available and no password
|
155
|
+
if @remember_token && !@password
|
156
|
+
credentials["remember-token"] = @remember_token
|
157
|
+
else
|
158
|
+
credentials["password"] = @password
|
159
|
+
end
|
160
|
+
|
161
|
+
credentials
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "keyring_store"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
module Tastytrade
|
7
|
+
# Manages session persistence and token storage
|
8
|
+
class SessionManager
|
9
|
+
SESSION_KEY_PREFIX = "session"
|
10
|
+
TOKEN_KEY_PREFIX = "token"
|
11
|
+
REMEMBER_KEY_PREFIX = "remember"
|
12
|
+
|
13
|
+
attr_reader :username, :environment
|
14
|
+
|
15
|
+
def initialize(username:, environment: "production")
|
16
|
+
@username = username
|
17
|
+
@environment = environment
|
18
|
+
end
|
19
|
+
|
20
|
+
# Save session data securely
|
21
|
+
#
|
22
|
+
# @param session [Tastytrade::Session] The session to save
|
23
|
+
# @param password [String] The password (only saved if remember is true)
|
24
|
+
# @param remember [Boolean] Whether to save credentials for auto-login
|
25
|
+
def save_session(session, password: nil, remember: false)
|
26
|
+
# Always save the session token
|
27
|
+
save_token(session.session_token)
|
28
|
+
|
29
|
+
# Save session expiration if available
|
30
|
+
save_session_expiration(session.session_expiration) if session.session_expiration
|
31
|
+
|
32
|
+
if remember && session.remember_token
|
33
|
+
save_remember_token(session.remember_token)
|
34
|
+
save_password(password) if password && KeyringStore.available?
|
35
|
+
end
|
36
|
+
|
37
|
+
# Save session metadata
|
38
|
+
config = CLIConfig.new
|
39
|
+
config.set("current_username", username)
|
40
|
+
config.set("environment", environment)
|
41
|
+
config.set("last_login", Time.now.to_s)
|
42
|
+
|
43
|
+
true
|
44
|
+
rescue StandardError => e
|
45
|
+
warn "Failed to save session: #{e.message}"
|
46
|
+
false
|
47
|
+
end
|
48
|
+
|
49
|
+
# Load saved session if available
|
50
|
+
#
|
51
|
+
# @return [Hash, nil] Session data or nil if not found
|
52
|
+
def load_session
|
53
|
+
token = load_token
|
54
|
+
return nil unless token
|
55
|
+
|
56
|
+
{
|
57
|
+
session_token: token,
|
58
|
+
remember_token: load_remember_token,
|
59
|
+
session_expiration: load_session_expiration,
|
60
|
+
username: username,
|
61
|
+
environment: environment
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
# Create a new session from saved credentials
|
66
|
+
#
|
67
|
+
# @return [Tastytrade::Session, nil] Authenticated session or nil
|
68
|
+
def restore_session
|
69
|
+
password = load_password
|
70
|
+
remember_token = load_remember_token
|
71
|
+
|
72
|
+
return nil unless password || remember_token
|
73
|
+
|
74
|
+
session = Session.new(
|
75
|
+
username: username,
|
76
|
+
password: password,
|
77
|
+
remember_token: remember_token,
|
78
|
+
is_test: environment == "sandbox"
|
79
|
+
)
|
80
|
+
|
81
|
+
session.login
|
82
|
+
session
|
83
|
+
rescue StandardError => e
|
84
|
+
warn "Failed to restore session: #{e.message}"
|
85
|
+
nil
|
86
|
+
end
|
87
|
+
|
88
|
+
# Clear all stored session data
|
89
|
+
def clear_session!
|
90
|
+
KeyringStore.delete(token_key)
|
91
|
+
KeyringStore.delete(remember_token_key)
|
92
|
+
KeyringStore.delete(password_key)
|
93
|
+
KeyringStore.delete(session_expiration_key)
|
94
|
+
|
95
|
+
config = CLIConfig.new
|
96
|
+
config.delete("current_username")
|
97
|
+
config.delete("last_login")
|
98
|
+
|
99
|
+
true
|
100
|
+
end
|
101
|
+
|
102
|
+
# Check if we have stored credentials
|
103
|
+
def saved_credentials?
|
104
|
+
!load_password.nil? || !load_remember_token.nil?
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def token_key
|
110
|
+
"#{TOKEN_KEY_PREFIX}_#{username}_#{environment}"
|
111
|
+
end
|
112
|
+
|
113
|
+
def remember_token_key
|
114
|
+
"#{REMEMBER_KEY_PREFIX}_#{username}_#{environment}"
|
115
|
+
end
|
116
|
+
|
117
|
+
def password_key
|
118
|
+
"password_#{username}_#{environment}"
|
119
|
+
end
|
120
|
+
|
121
|
+
def session_expiration_key
|
122
|
+
"#{SESSION_KEY_PREFIX}_expiration_#{username}_#{environment}"
|
123
|
+
end
|
124
|
+
|
125
|
+
def save_token(token)
|
126
|
+
KeyringStore.set(token_key, token)
|
127
|
+
end
|
128
|
+
|
129
|
+
def load_token
|
130
|
+
KeyringStore.get(token_key)
|
131
|
+
end
|
132
|
+
|
133
|
+
def save_remember_token(token)
|
134
|
+
KeyringStore.set(remember_token_key, token)
|
135
|
+
end
|
136
|
+
|
137
|
+
def load_remember_token
|
138
|
+
KeyringStore.get(remember_token_key)
|
139
|
+
end
|
140
|
+
|
141
|
+
def save_password(password)
|
142
|
+
KeyringStore.set(password_key, password)
|
143
|
+
end
|
144
|
+
|
145
|
+
def load_password
|
146
|
+
KeyringStore.get(password_key)
|
147
|
+
end
|
148
|
+
|
149
|
+
def save_session_expiration(expiration)
|
150
|
+
KeyringStore.set(session_expiration_key, expiration.iso8601)
|
151
|
+
end
|
152
|
+
|
153
|
+
def load_session_expiration
|
154
|
+
value = KeyringStore.get(session_expiration_key)
|
155
|
+
value ? Time.parse(value).iso8601 : nil
|
156
|
+
rescue StandardError
|
157
|
+
nil
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
data/lib/tastytrade.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Unofficial Ruby SDK for Tastytrade API
|
4
|
+
#
|
5
|
+
# IMPORTANT DISCLAIMER:
|
6
|
+
# This is an unofficial SDK and is not affiliated with, endorsed by, or
|
7
|
+
# sponsored by Tastytrade, Tastyworks, or any of their affiliates.
|
8
|
+
#
|
9
|
+
# Trading financial instruments involves substantial risk and may result in
|
10
|
+
# loss of capital. This software is provided for educational purposes only.
|
11
|
+
# Always consult with a qualified financial advisor before making investment decisions.
|
12
|
+
|
13
|
+
require_relative "tastytrade/version"
|
14
|
+
require_relative "tastytrade/client"
|
15
|
+
require_relative "tastytrade/models"
|
16
|
+
require_relative "tastytrade/session"
|
17
|
+
|
18
|
+
module Tastytrade
|
19
|
+
class Error < StandardError; end
|
20
|
+
|
21
|
+
# Authentication errors
|
22
|
+
class AuthenticationError < Error; end
|
23
|
+
class SessionExpiredError < AuthenticationError; end
|
24
|
+
class TokenRefreshError < AuthenticationError; end
|
25
|
+
class InvalidCredentialsError < AuthenticationError; end
|
26
|
+
class NetworkTimeoutError < Error; end
|
27
|
+
|
28
|
+
# API URLs
|
29
|
+
API_URL = "https://api.tastyworks.com"
|
30
|
+
CERT_URL = "https://api.cert.tastyworks.com"
|
31
|
+
end
|
data/sig/tastytrade.rbs
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
require "open3"
|
5
|
+
|
6
|
+
RSpec.describe "tastytrade executable" do
|
7
|
+
let(:exe_path) { File.expand_path("../../exe/tastytrade", __dir__) }
|
8
|
+
|
9
|
+
def run_command(*args)
|
10
|
+
cmd = [exe_path] + args
|
11
|
+
stdout, stderr, status = Open3.capture3(*cmd)
|
12
|
+
{ stdout: stdout, stderr: stderr, status: status }
|
13
|
+
end
|
14
|
+
|
15
|
+
it "exists and is executable" do
|
16
|
+
expect(File.exist?(exe_path)).to be true
|
17
|
+
expect(File.executable?(exe_path)).to be true
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "version command" do
|
21
|
+
it "displays version information" do
|
22
|
+
result = run_command("version")
|
23
|
+
expect(result[:status].success?).to be true
|
24
|
+
expect(result[:stdout]).to include("Tastytrade CLI v#{Tastytrade::VERSION}")
|
25
|
+
end
|
26
|
+
|
27
|
+
it "works with --version flag" do
|
28
|
+
result = run_command("--version")
|
29
|
+
expect(result[:status].success?).to be true
|
30
|
+
expect(result[:stdout]).to include(Tastytrade::VERSION)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "help command" do
|
35
|
+
it "displays help information" do
|
36
|
+
result = run_command("help")
|
37
|
+
expect(result[:status].success?).to be true
|
38
|
+
expect(result[:stdout]).to include("Tastytrade commands:")
|
39
|
+
expect(result[:stdout]).to include("login")
|
40
|
+
expect(result[:stdout]).to include("accounts")
|
41
|
+
expect(result[:stdout]).to include("balance")
|
42
|
+
end
|
43
|
+
|
44
|
+
it "works with --help flag" do
|
45
|
+
result = run_command("--help")
|
46
|
+
expect(result[:status].success?).to be true
|
47
|
+
expect(result[:stdout]).to include("Tastytrade commands:")
|
48
|
+
end
|
49
|
+
|
50
|
+
it "works with -h flag" do
|
51
|
+
result = run_command("-h")
|
52
|
+
expect(result[:status].success?).to be true
|
53
|
+
expect(result[:stdout]).to include("Tastytrade commands:")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "global options" do
|
58
|
+
it "accepts --test flag" do
|
59
|
+
result = run_command("help", "--test")
|
60
|
+
expect(result[:status].success?).to be true
|
61
|
+
expect(result[:stdout]).to include("Tastytrade commands:")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "invalid commands" do
|
66
|
+
it "shows error for unknown command" do
|
67
|
+
result = run_command("invalid-command")
|
68
|
+
expect(result[:status].success?).to be false
|
69
|
+
expect(result[:stderr]).to include("Could not find command").or include("Unknown command")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "command stubs" do
|
74
|
+
it "has login command" do
|
75
|
+
result = run_command("help", "login")
|
76
|
+
expect(result[:status].success?).to be true
|
77
|
+
expect(result[:stdout]).to include("Login to Tastytrade")
|
78
|
+
end
|
79
|
+
|
80
|
+
it "has accounts command" do
|
81
|
+
result = run_command("help", "accounts")
|
82
|
+
expect(result[:status].success?).to be true
|
83
|
+
expect(result[:stdout]).to include("List all accounts")
|
84
|
+
end
|
85
|
+
|
86
|
+
it "has select command" do
|
87
|
+
result = run_command("help", "select")
|
88
|
+
expect(result[:status].success?).to be true
|
89
|
+
expect(result[:stdout]).to include("Select an account to use")
|
90
|
+
end
|
91
|
+
|
92
|
+
it "has logout command" do
|
93
|
+
result = run_command("help", "logout")
|
94
|
+
expect(result[:status].success?).to be true
|
95
|
+
expect(result[:stdout]).to include("Logout from Tastytrade")
|
96
|
+
end
|
97
|
+
|
98
|
+
it "has balance command" do
|
99
|
+
result = run_command("help", "balance")
|
100
|
+
expect(result[:status].success?).to be true
|
101
|
+
expect(result[:stdout]).to include("Display account balance")
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
unless ENV["DISABLE_SIMPLECOV"]
|
4
|
+
require "simplecov"
|
5
|
+
SimpleCov.start do
|
6
|
+
add_filter "/spec/"
|
7
|
+
add_filter "/bin/"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
require "bundler/setup"
|
12
|
+
require "tastytrade"
|
13
|
+
require "webmock/rspec"
|
14
|
+
|
15
|
+
RSpec.configure do |config|
|
16
|
+
# Enable flags like --only-failures and --next-failure
|
17
|
+
config.example_status_persistence_file_path = ".rspec_status"
|
18
|
+
|
19
|
+
# Disable RSpec exposing methods globally on `Module` and `main`
|
20
|
+
config.disable_monkey_patching!
|
21
|
+
|
22
|
+
config.expect_with :rspec do |c|
|
23
|
+
c.syntax = :expect
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|