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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/commands/release-pr.md +108 -0
  3. data/.github/ISSUE_TEMPLATE/feature_request.md +29 -0
  4. data/.github/ISSUE_TEMPLATE/roadmap_task.md +34 -0
  5. data/.github/dependabot.yml +11 -0
  6. data/.github/workflows/main.yml +75 -0
  7. data/.rspec +3 -0
  8. data/.rubocop.yml +101 -0
  9. data/.ruby-version +1 -0
  10. data/CHANGELOG.md +100 -0
  11. data/CLAUDE.md +78 -0
  12. data/CODE_OF_CONDUCT.md +81 -0
  13. data/CONTRIBUTING.md +89 -0
  14. data/DISCLAIMER.md +54 -0
  15. data/LICENSE.txt +24 -0
  16. data/README.md +235 -0
  17. data/ROADMAP.md +157 -0
  18. data/Rakefile +17 -0
  19. data/SECURITY.md +48 -0
  20. data/docs/getting_started.md +48 -0
  21. data/docs/python_sdk_analysis.md +181 -0
  22. data/exe/tastytrade +8 -0
  23. data/lib/tastytrade/cli.rb +604 -0
  24. data/lib/tastytrade/cli_config.rb +79 -0
  25. data/lib/tastytrade/cli_helpers.rb +178 -0
  26. data/lib/tastytrade/client.rb +117 -0
  27. data/lib/tastytrade/keyring_store.rb +72 -0
  28. data/lib/tastytrade/models/account.rb +129 -0
  29. data/lib/tastytrade/models/account_balance.rb +75 -0
  30. data/lib/tastytrade/models/base.rb +47 -0
  31. data/lib/tastytrade/models/current_position.rb +155 -0
  32. data/lib/tastytrade/models/user.rb +23 -0
  33. data/lib/tastytrade/models.rb +7 -0
  34. data/lib/tastytrade/session.rb +164 -0
  35. data/lib/tastytrade/session_manager.rb +160 -0
  36. data/lib/tastytrade/version.rb +5 -0
  37. data/lib/tastytrade.rb +31 -0
  38. data/sig/tastytrade.rbs +4 -0
  39. data/spec/exe/tastytrade_spec.rb +104 -0
  40. data/spec/spec_helper.rb +26 -0
  41. data/spec/tastytrade/cli_accounts_spec.rb +166 -0
  42. data/spec/tastytrade/cli_auth_spec.rb +216 -0
  43. data/spec/tastytrade/cli_config_spec.rb +180 -0
  44. data/spec/tastytrade/cli_helpers_spec.rb +248 -0
  45. data/spec/tastytrade/cli_interactive_spec.rb +54 -0
  46. data/spec/tastytrade/cli_logout_spec.rb +121 -0
  47. data/spec/tastytrade/cli_select_spec.rb +174 -0
  48. data/spec/tastytrade/cli_status_spec.rb +206 -0
  49. data/spec/tastytrade/client_spec.rb +210 -0
  50. data/spec/tastytrade/keyring_store_spec.rb +168 -0
  51. data/spec/tastytrade/models/account_balance_spec.rb +247 -0
  52. data/spec/tastytrade/models/account_spec.rb +206 -0
  53. data/spec/tastytrade/models/base_spec.rb +61 -0
  54. data/spec/tastytrade/models/current_position_spec.rb +444 -0
  55. data/spec/tastytrade/models/user_spec.rb +58 -0
  56. data/spec/tastytrade/session_manager_spec.rb +296 -0
  57. data/spec/tastytrade/session_spec.rb +392 -0
  58. data/spec/tastytrade_spec.rb +9 -0
  59. 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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "models/base"
4
+ require_relative "models/user"
5
+ require_relative "models/account"
6
+ require_relative "models/account_balance"
7
+ require_relative "models/current_position"
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tastytrade
4
+ VERSION = "0.2.0"
5
+ 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
@@ -0,0 +1,4 @@
1
+ module Tastytrade
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
@@ -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
@@ -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