├── .rspec ├── lib ├── tdameritrade_api │ ├── version.rb │ ├── constants.rb │ ├── tdameritrade_api_error.rb │ ├── balances_and_positions.rb │ ├── bindata_types.rb │ ├── client.rb │ ├── streamer_types.rb │ ├── watchlist.rb │ ├── price_history.rb │ └── streamer.rb ├── tdameritrade_api.rb └── tasks │ └── save_stream.rake ├── .travis.yml ├── spec ├── test_data │ ├── sample_stream.binary │ ├── watchlist.txt │ └── sample_stream_20140807.binary ├── tdameritrade_api │ ├── client_spec.rb │ ├── watchlist_spec.rb │ ├── balances_and_positions_spec.rb │ ├── price_history_spec.rb │ └── streamer_spec.rb └── spec_helper.rb ├── vendor ├── docs │ ├── TDAMERITRADE-CONSUMER-API.pdf │ ├── tdameritrade-api-docs-v221.chm │ └── curl_test_strings.txt └── tmp.xml ├── Gemfile ├── .gitignore ├── Rakefile ├── LICENSE.txt ├── tdameritrade_api.gemspec ├── CHANGELOG.md └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | -------------------------------------------------------------------------------- /lib/tdameritrade_api/version.rb: -------------------------------------------------------------------------------- 1 | module TDAmeritradeApi 2 | VERSION = "1.2.2" 3 | end 4 | -------------------------------------------------------------------------------- /lib/tdameritrade_api/constants.rb: -------------------------------------------------------------------------------- 1 | module TDAmeritradeApi 2 | module Constants 3 | DEFAULT_TIMEOUT=10 4 | end 5 | end -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2.0 4 | gemfile: 5 | - Gemfile 6 | script: "rspec" 7 | notifications: 8 | email: false -------------------------------------------------------------------------------- /spec/test_data/sample_stream.binary: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wakproductions/tdameritrade_api/HEAD/spec/test_data/sample_stream.binary -------------------------------------------------------------------------------- /lib/tdameritrade_api.rb: -------------------------------------------------------------------------------- 1 | require 'tdameritrade_api/version' 2 | require 'tdameritrade_api/client' 3 | 4 | module TDAmeritradeApi 5 | 6 | end -------------------------------------------------------------------------------- /vendor/docs/TDAMERITRADE-CONSUMER-API.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wakproductions/tdameritrade_api/HEAD/vendor/docs/TDAMERITRADE-CONSUMER-API.pdf -------------------------------------------------------------------------------- /vendor/docs/tdameritrade-api-docs-v221.chm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wakproductions/tdameritrade_api/HEAD/vendor/docs/tdameritrade-api-docs-v221.chm -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in tdameritrade_api.gemspec 4 | gemspec 5 | 6 | group :test do 7 | gem "pry", "~> 0.10.2" 8 | end 9 | -------------------------------------------------------------------------------- /lib/tdameritrade_api/tdameritrade_api_error.rb: -------------------------------------------------------------------------------- 1 | module TDAmeritradeApi 2 | class TDAmeritradeApiError < RuntimeError 3 | 4 | end 5 | 6 | class Exception 7 | def is_ctrl_c_exception? 8 | [SystemExit, Interrupt, IRB::Abort].index(self.class).present? 9 | end 10 | end 11 | end -------------------------------------------------------------------------------- /spec/tdameritrade_api/client_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe TDAmeritradeApi::Client do 4 | # All of the specs in this file have been moved to price_history_spec.rb 5 | # The main thing to be tested for client.rb is the login feature, and that is invoked in the before(:suite) hook 6 | end -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | .idea 7 | .DS_STORE 8 | spec/test_data/sample_stream_rspec_test.binary 9 | Gemfile.lock 10 | InstalledFiles 11 | _yardoc 12 | coverage 13 | doc/ 14 | lib/bundler/man 15 | pkg 16 | rdoc 17 | spec/reports 18 | spec/test_data/sample_stream_archives 19 | test/tmp 20 | test/version_tmp 21 | tmp 22 | Gemfile.lock 23 | -------------------------------------------------------------------------------- /spec/tdameritrade_api/watchlist_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tdameritrade_api' 3 | 4 | describe TDAmeritradeApi::Client do 5 | let(:client) { RSpec.configuration.client } 6 | 7 | it "should be able to get the watchlists" do 8 | w = client.get_watchlists 9 | expect(w).to be_a(Array) 10 | expect(w.count).to be > 0 11 | wl = w.first 12 | 13 | expect(wl[:name]).to be_a(String) 14 | expect(wl[:id]).to be_a(String) 15 | expect(wl[:symbols]).to be_a(Array) 16 | expect(wl[:symbols].first).to be_a(String) 17 | end 18 | 19 | it "should be able to get a specific watchlist given a specific id" 20 | it "can create, edit, and delete a watchlist" # a comprehensive test 21 | end -------------------------------------------------------------------------------- /spec/tdameritrade_api/balances_and_positions_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tdameritrade_api' 3 | require 'pry' 4 | 5 | describe TDAmeritradeApi::Client do 6 | let(:client) { RSpec.configuration.client } 7 | 8 | it 'gets the balances and positions hash' do 9 | account_id = client.accounts.first[:account_id] 10 | bp = client.get_balances_and_positions(account_id) 11 | 12 | expect(validate_bp_return_hash(bp)).to be_truthy 13 | end 14 | 15 | private 16 | 17 | def validate_bp_return_hash(hash) 18 | return false unless hash.has_key? "error" 19 | return false unless hash.has_key? "account_id" 20 | return false unless hash["error"].nil? 21 | 22 | return true 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require 'tdameritrade_api' 3 | 4 | task :perform_action do 5 | c = TDAmeritradeApi::Client.new 6 | c.login 7 | #c.session_id="128459556EEA989391FBAAA5E2BF8EB4.cOr5v8xckaAXQxWmG7bn2g" 8 | #prices = c.get_minute_price_history('GM',10) 9 | prices = c.get_daily_price_history('FOX', '20140105', '20140123') 10 | of = open(File.join(Dir.getwd, "output.txt"), "w") 11 | prices.each do |bar| 12 | of.write "#{bar[:open]} #{bar[:high]} #{bar[:low]} #{bar[:close]} #{bar[:timestamp]}\n" 13 | end 14 | of.close 15 | end 16 | 17 | task :test_bp do 18 | c = TDAmeritradeApi::Client.new 19 | c.login 20 | 21 | options = {:type => 'p', 22 | :suppressquote => 'true'} 23 | bp = c.get_balances_and_positions(options) 24 | p bp 25 | end 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 wkotzan 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'tdameritrade_api' 2 | 3 | # This file was generated by the `rspec --init` command. Conventionally, all 4 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 5 | # Require this file using `require "spec_helper"` to ensure that it is only 6 | # loaded once. 7 | # 8 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 9 | RSpec.configure do |config| 10 | config.treat_symbols_as_metadata_keys_with_true_values = true 11 | config.run_all_when_everything_filtered = true 12 | config.filter_run :focus 13 | 14 | # Run specs in random order to surface order dependencies. If you find an 15 | # order dependency and want to debug it, you can fix the order by providing 16 | # the seed, which is printed after each run. 17 | # --seed 1234 18 | config.order = 'random' 19 | 20 | # This is sort of a test in that you can't really do anything unless you are logged in 21 | # We have this hook login here so that you only log in once and don't have to log in for each test 22 | config.add_setting :client 23 | config.before(:suite) do 24 | RSpec.configuration.client = TDAmeritradeApi::Client.new 25 | RSpec.configuration.client.login 26 | end 27 | 28 | end 29 | 30 | -------------------------------------------------------------------------------- /tdameritrade_api.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'tdameritrade_api/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "tdameritrade_api" 8 | spec.version = TDAmeritradeApi::VERSION 9 | spec.authors = ["Winston Kotzan"] 10 | spec.email = ["wak@wakproductions.com"] 11 | spec.summary = %q{This is a simple gem for connecting to the TD Ameritrade API} 12 | spec.description = %q{Only contains limited functionality} 13 | spec.homepage = "" 14 | spec.license = "MIT" 15 | 16 | spec.files = [`git ls-files`.split($/)] + Dir["lib/**/*"] 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_development_dependency "rspec", "~> 3.2" 22 | spec.add_dependency "bundler", "~> 1.5" 23 | spec.add_dependency "rake" 24 | spec.add_dependency "bindata", ">= 1.8" 25 | spec.add_dependency "httparty", "~> 0.13" 26 | spec.add_dependency "activesupport", "~> 4.0" 27 | spec.add_dependency "nokogiri", "~> 1.6" 28 | end 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### v1.2.2 2 | 3 | Changed #edit_watchlist to send parameters as POST vs GET to avoid `Request-URI Too Large` error. 4 | 5 | ### v1.2.1 6 | 7 | Removed accidental binding.pry statements. 8 | 9 | ### v1.2.0 10 | 11 | Added feature to view, create, and edit watchlists available in ThinkOrSwim. 12 | 13 | ### v1.1.1 14 | 15 | Added missing fields to the output of #get_quote, the real-time quote retrieval method. Now includes 16 | error (if any), description, year high/low, exchange, asset type. 17 | 18 | ### v1.1.0 19 | 20 | Added ability to retrieve account balances and positions. 21 | 22 | ### v1.0.20150422 Known Issues 23 | 24 | steamer_spec.rb has a flickering test in that it fails outside of the US/Eastern time zone. The problem 25 | is in the TDAmeritradeApi::StreamerTypes::StreamData.convert_time_columns in converting the time. The 26 | TD Ameritrade system gives trade_time and quote_time in number of seconds since midnight Eastern Time. 27 | Converting this to a DateTime object requires doing DateTime.today (or whichever day) converted 28 | to a Time object + the value in trade_time or quote_time. The problem is that when you build a DateTime 29 | object and convert it to Time, Ruby uses the local time zone. To fix this you need to convert the input 30 | of convert_time_columns(day) into an Eastern Time Zone midnight value. This will be fixed soon but 31 | for now I am disabling that particular validation in the test. -------------------------------------------------------------------------------- /lib/tdameritrade_api/balances_and_positions.rb: -------------------------------------------------------------------------------- 1 | module TDAmeritradeApi 2 | module BalancesAndPositions 3 | BALANCES_AND_POSITIONS_URL='https://apis.tdameritrade.com/apps/100/BalancesAndPositions' 4 | 5 | # +get_balances_and_positions+ get account balances 6 | # +options+ may contain any of the params outlined in the API docs 7 | # * accountid - one of the account ids returned from the login service 8 | # * type - type of data to be returned ('b' or 'p') 9 | # * suppress_quotes - whether or not quotes should be suppressed on the positions (true/false) 10 | # * alt_balance_format - whether or not the balances response should be returned in alternative format (true/false) 11 | def get_balances_and_positions(account_id, options={}) 12 | request_params = build_bp_params(account_id, options) 13 | 14 | uri = URI.parse BALANCES_AND_POSITIONS_URL 15 | uri.query = URI.encode_www_form(request_params) 16 | 17 | response = HTTParty.get(uri, headers: {'Cookie' => "JSESSIONID=#{@session_id}"}, timeout: 10) 18 | if response.code != 200 19 | raise TDAmeritradeApiError, "HTTP response #{response.code}: #{response.body}" 20 | end 21 | 22 | bp_hash = {"error"=>"failed"} 23 | result_hash = Hash.from_xml(response.body.to_s) 24 | if result_hash['amtd']['result'] == 'OK' then 25 | bp_hash = result_hash['amtd']['positions'] 26 | end 27 | 28 | bp_hash 29 | rescue Exception => e 30 | raise TDAmeritradeApiError, "error in get_balances_and_positions() - #{e.message}" if !e.is_ctrl_c_exception? 31 | end 32 | 33 | private 34 | 35 | def build_bp_params(account_id, options) 36 | {source: @source_id, accountid: account_id}.merge(options) 37 | end 38 | end 39 | end -------------------------------------------------------------------------------- /lib/tasks/save_stream.rake: -------------------------------------------------------------------------------- 1 | namespace :tdameritrade_api do 2 | 3 | # This is a very barebones utility. I can add more configuration features to this if anyone finds it useful. 4 | desc "Saves a TD Ameritrade Level 1 stream to file for later backtesting" 5 | task :save_stream => :environment do 6 | i = 0 7 | file_num = 1 8 | request_fields = [:volume, :last, :bid, :symbol, :ask, :quotetime, :high, :low, :close, :tradetime, :tick] 9 | symbols = watchlist 10 | file_name = 11 | 12 | while true 13 | begin 14 | file_num += 1 while File.exists? File.join(Dir.pwd, 'spec', 'test_data', 'sample_stream_archives', new_mock_data_file_name(file_num)) 15 | streamer.output_file = File.join(Dir.pwd, 'spec', 'test_data', 'sample_stream_archives', new_mock_data_file_name(file_num)) if use_mock_data_file 16 | streamer.run(symbols: symbols, request_fields: request_fields) do |data| 17 | data.convert_time_columns 18 | case data.stream_data_type 19 | when :heartbeat 20 | pout "Heartbeat: #{data.timestamp}" 21 | when :snapshot 22 | if data.service_id == "100" 23 | pout "Snapshot SID-#{data.service_id}: #{data.columns[:description]}" 24 | else 25 | pout "Snapshot: #{data}" 26 | end 27 | when :stream_data 28 | pout "#{i} Stream: #{data.columns}" 29 | i += 1 30 | else 31 | pout "Unknown type of data: #{data}" 32 | end 33 | end 34 | rescue Exception => e 35 | # This idiom of a rescue block you can use to reset the connection if it drops, 36 | # which can happen easily during a fast market. 37 | if e.class == Errno::ECONNRESET 38 | puts "Connection reset, reconnecting..." 39 | else 40 | raise e 41 | end 42 | end 43 | end 44 | 45 | end 46 | end -------------------------------------------------------------------------------- /lib/tdameritrade_api/bindata_types.rb: -------------------------------------------------------------------------------- 1 | require 'bindata' 2 | 3 | module TDAmeritradeApi 4 | module BinDataTypes 5 | class PriceHistoryHeader < BinData::Record 6 | int32be :symbol_count 7 | end 8 | 9 | class PriceHistorySymbolData < BinData::Record 10 | int16be :symbol_length 11 | string :symbol, :read_length=>:symbol_length 12 | int8be :error_code 13 | int16be :error_length, :onlyif => :has_error? 14 | string :error_text, :onlyif => :has_error?, :length=>:error_length 15 | int32be :bar_count 16 | 17 | def has_error? 18 | error_code != 0 19 | end 20 | end 21 | 22 | # IMPORTANT NOTE (4/23/15): 23 | # When using currency in Ruby, it is recommended to use BigDecimal instead of 24 | # float types due to rounding errors with the float type. I have discovered that in 25 | # Ruby 2.2 you may have issues with ActiveRecord if your database stores these values 26 | # as a decimal type because this gem outputs the close, high, low, open, and volume 27 | # values in float type. The fix is either to have rounding through a precision value 28 | # in your database field or to typecast the output of TDAmeritradeAPI gem to BigDecimal. 29 | # I currently have no plans to change the code here since a 4-byte float is what is 30 | # used by TD Ameritrade's system and I want to keep the output of this gem consistent 31 | # with the specs used by TDA. 32 | class PriceHistoryBarRaw < BinData::Record 33 | float_be :close # may have to round this on a 64 bit system 34 | float_be :high # may have to round this on a 64 bit system 35 | float_be :low # may have to round this on a 64 bit system 36 | float_be :open # may have to round this on a 64 bit system 37 | float_be :volume # in 100s 38 | int64be :timestampint # number of milliseconds - needs to be converted to seconds for Ruby 39 | end 40 | 41 | end 42 | end -------------------------------------------------------------------------------- /lib/tdameritrade_api/client.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'openssl' 3 | require 'httparty' 4 | require 'active_support/core_ext/hash/conversions' 5 | require 'tdameritrade_api/bindata_types' 6 | require 'tdameritrade_api/tdameritrade_api_error' 7 | require 'tdameritrade_api/price_history' 8 | require 'tdameritrade_api/streamer' 9 | require 'tdameritrade_api/watchlist' 10 | require 'tdameritrade_api/balances_and_positions' 11 | 12 | module TDAmeritradeApi 13 | class Client 14 | include PriceHistory 15 | include Streamer 16 | include Watchlist 17 | include BalancesAndPositions 18 | 19 | attr_accessor :source_id, :user_id, :password 20 | attr_reader :login_response, :session_id, :accounts 21 | 22 | def initialize 23 | self.source_id=ENV['TDAMERITRADE_SOURCE_KEY'] 24 | self.user_id=ENV['TDAMERITRADE_USER_ID'] 25 | self.password=ENV['TDAMERITRADE_PASSWORD'] 26 | end 27 | 28 | def login 29 | clear_login_data 30 | uri = URI.parse("https://apis.tdameritrade.com/apps/100/LogIn?source=#{@source_id}&version=1.0.0") 31 | http = Net::HTTP.new(uri.host, uri.port) 32 | http.use_ssl = true 33 | request = Net::HTTP::Post.new(uri.path) 34 | request.add_field('Content-Type', 'application/x-www-form-urlencoded') 35 | request.body = "userid=#{@user_id}&password=#{@password}&source=#{@source_id}&version=1.0.0" 36 | result = http.request(request) 37 | @login_response = result.body 38 | 39 | parse_login_response if login_success? 40 | login_success? 41 | end 42 | 43 | def login_success? 44 | return false if @login_response.nil? 45 | begin 46 | login_result = @login_response.scan(/(.*)<\/result>/).first.first 47 | rescue 48 | return false 49 | end 50 | login_result && (login_result == "OK") 51 | end 52 | 53 | private 54 | 55 | def parse_login_response 56 | @session_id = @login_response.scan(/(.*)<\/session-id>/).first.first 57 | @accounts = Array.new 58 | r = Nokogiri::XML::Document.parse @login_response 59 | r.xpath('/amtd/xml-log-in/accounts/account').each do |account| 60 | a = Hash.new 61 | a[:account_id] = account.xpath('account-id').text 62 | a[:display_name] = account.xpath('display-name').text 63 | a[:description] = account.xpath('description').text 64 | a[:company] = account.xpath('company').text 65 | a[:segment] = account.xpath('segment').text 66 | @accounts << a 67 | end 68 | end 69 | 70 | def clear_login_data 71 | @login_response = nil 72 | @session_id = nil 73 | @accounts = nil 74 | end 75 | 76 | end 77 | end -------------------------------------------------------------------------------- /spec/test_data/watchlist.txt: -------------------------------------------------------------------------------- 1 | ADBE 2 | APPY 3 | ARIA 4 | ATHM 5 | BEAT 6 | BIDU 7 | CALL 8 | CPST 9 | CRM 10 | CSIQ 11 | DANG 12 | DRYS 13 | DSCI 14 | EBAY 15 | EBIX 16 | EGLE 17 | ENDP 18 | ERII 19 | FSLR 20 | FUEL 21 | GIMO 22 | GME 23 | GOGO 24 | GTAT 25 | HART 26 | HZNP 27 | ICPT 28 | INO 29 | INVN 30 | ISR 31 | ITMN 32 | JASO 33 | JCP 34 | KNDI 35 | LIOX 36 | LNKD 37 | MNGA 38 | MOBI 39 | MRVC 40 | MY 41 | NMBL 42 | NPSP 43 | NUVA 44 | OXGN 45 | PANW 46 | PLUG 47 | PXLW 48 | QIHU 49 | QTWW 50 | REE 51 | REPH 52 | RFMD 53 | RIGL 54 | SCTY 55 | SFUN 56 | SINA 57 | SODA 58 | SPX 59 | SWIR 60 | TRIP 61 | TWTR 62 | UNIS 63 | VIX 64 | VXX 65 | WBAI 66 | YELP 67 | YNDX 68 | YOD 69 | YRCW 70 | YY 71 | Z 72 | ZU 73 | AAL 74 | AAPL 75 | ACE 76 | ACN 77 | ADM 78 | ADSK 79 | AET 80 | AIG 81 | AJG 82 | AKAM 83 | AKS 84 | ALB 85 | ALK 86 | ALL 87 | AMAT 88 | AMZN 89 | APA 90 | ARUN 91 | ATHN 92 | AVAV 93 | BA 94 | BABY 95 | BBT 96 | BBY 97 | BK 98 | BMRN 99 | BRCM 100 | BTU 101 | BX 102 | CAR 103 | CAT 104 | CERN 105 | CHGG 106 | CHK 107 | CLX 108 | CMG 109 | CMI 110 | CNQR 111 | COF 112 | COH 113 | COLB 114 | CREE 115 | CSX 116 | CTRP 117 | CUDA 118 | CVS 119 | CVX 120 | DAL 121 | DATA 122 | DD 123 | DE 124 | DFS 125 | DIS 126 | DNKN 127 | ED 128 | EMC 129 | EOG 130 | ESRX 131 | ETN 132 | ETP 133 | EVHC 134 | EXC 135 | FANG 136 | FB 137 | FEYE 138 | FITB 139 | FTNT 140 | GAS 141 | GCI 142 | GDP 143 | GIII 144 | GILD 145 | GLD 146 | GM 147 | GMCR 148 | GMT 149 | GOMO 150 | GOOG 151 | GPN 152 | GPS 153 | GS 154 | GY 155 | HA 156 | HBI 157 | HCN 158 | HCP 159 | HD 160 | HEP 161 | HOG 162 | HON 163 | HRB 164 | HTZ 165 | HUM 166 | I 167 | IBKR 168 | IBM 169 | IDXX 170 | INFY 171 | INTU 172 | IOC 173 | IRM 174 | JAZZ 175 | JBLU 176 | JKS 177 | JWN 178 | K 179 | KATE 180 | KEY 181 | KMP 182 | KORS 183 | KRFT 184 | KSS 185 | LH 186 | LLL 187 | LOGM 188 | LULU 189 | LVS 190 | LXFT 191 | LXK 192 | LZB 193 | MA 194 | MCD 195 | MO 196 | MON 197 | MRK 198 | MRVL 199 | MU 200 | MYL 201 | N 202 | NFLX 203 | NKE 204 | NSC 205 | NTAP 206 | NYCB 207 | OAS 208 | OKS 209 | ORCL 210 | OXY 211 | PAY 212 | PAYX 213 | PBI 214 | PBPB 215 | PBYI 216 | PCLN 217 | PG 218 | PM 219 | POT 220 | PVH 221 | PVTB 222 | QCOM 223 | QLIK 224 | QSII 225 | RATE 226 | REGN 227 | RHT 228 | RJET 229 | RJF 230 | RRGB 231 | SBNY 232 | SIAL 233 | SIMO 234 | SLB 235 | SNDK 236 | SONC 237 | SWKS 238 | SYNA 239 | TCS 240 | TDC 241 | TGT 242 | THI 243 | TIF 244 | TJX 245 | TM 246 | TRLA 247 | TSCO 248 | TSLA 249 | TXN 250 | UA 251 | UAL 252 | ULTA 253 | USB 254 | UTX 255 | V 256 | VZ 257 | WAG 258 | WDAY 259 | WEN 260 | WFC 261 | WFM 262 | WLP 263 | WOOF 264 | WSM 265 | WUBA 266 | WWAV 267 | WYNN 268 | X 269 | YUM -------------------------------------------------------------------------------- /spec/tdameritrade_api/price_history_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tdameritrade_api' 3 | 4 | describe TDAmeritradeApi::Client do 5 | let(:client) { RSpec.configuration.client } 6 | let(:ticker_symbol) { 'PG' } 7 | let(:ticker_symbols) { ['SNDK', 'WDC', 'MU'] } 8 | 9 | it "should populate certain instance variables after logging in" do 10 | 11 | end 12 | 13 | it "should retrieve the last 2 days of 30 min data" do 14 | result = client.get_price_history(ticker_symbol, intervaltype: :minute, intervalduration: 30, periodtype: :day, period: 2).first[:bars] 15 | expect(result).to be_a Array 16 | expect(result.length).to eq(26) # 13 half hour periods in a trading day (not including extended hours) times 2 17 | validate_price_bar(result.first) 18 | end 19 | 20 | it "should retrieve a date range of data" do 21 | result = client.get_price_history(ticker_symbol, intervaltype: :daily, intervalduration: 1, startdate: Date.new(2014,7,22), enddate: Date.new(2014,7,25)).first[:bars] 22 | expect(result).to be_a Array 23 | expect(result.length).to eq(4) 24 | validate_price_bar(result.first) 25 | end 26 | 27 | it "should be able to get recent daily price history using get_daily_price_history" do 28 | result = client.get_daily_price_history ticker_symbol, '20140707', '20140707' 29 | #=> [{:open=>14.88, :high=>15.58, :low=>14.65, :close=>14.85, :volume=>36713.1, :timestamp=>2014-07-07 00:00:00 -0400, :interval=>:day}] 30 | 31 | expect(result).to be_a Array 32 | expect(result.length).to eq(1) 33 | validate_price_bar(result.first) 34 | end 35 | 36 | it "should be able to get the recent price history for multiple symbols at a time" do 37 | result = client.get_price_history(ticker_symbols, intervaltype: :daily, intervalduration: 1, startdate: Date.new(2015,2,2), enddate: Date.new(2015,2,12)) 38 | #=> [ 39 | # {:symbol=>'SNDK', :bars=>[{:open=>..., :high=>..., ..., ...},{:open=>...}]}, 40 | # {:symbol=>'WDC', :bars=>...}, 41 | # {:symbol=>'MU', :bars=>...} 42 | # ] 43 | 44 | expect(result).to be_a Array 45 | expect(result.length).to eq(3) 46 | 47 | first_result = result[0] 48 | expect(first_result).to have_key :symbol 49 | expect(first_result).to have_key :bars 50 | validate_price_bar(first_result[:bars].first) 51 | end 52 | 53 | it "should not be able to get any data unless logged in" do 54 | 55 | end 56 | 57 | it "should be able to download the daily data for a stock" do 58 | 59 | end 60 | 61 | it "should be able to download the minute history data for a stock" do 62 | 63 | end 64 | 65 | private 66 | def validate_price_bar(price_bar) 67 | expect(price_bar[:open]).to be_a_kind_of Numeric 68 | expect(price_bar[:high]).to be_a_kind_of Numeric 69 | expect(price_bar[:low]).to be_a_kind_of Numeric 70 | expect(price_bar[:close]).to be_a_kind_of Numeric 71 | expect(price_bar[:volume]).to be_a_kind_of Numeric 72 | expect(price_bar[:timestamp]).to be_a_kind_of Time 73 | end 74 | 75 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TD Ameritrade API gem for Ruby 2 | 3 | [![Build Status](https://travis-ci.org/wakproductions/tdameritrade_api.svg?branch=master)](https://travis-ci.org/wakproductions/tdameritrade_api) [![Code Climate](https://codeclimate.com/github/wakproductions/tdameritrade_api/badges/gpa.svg)](https://codeclimate.com/github/wakproductions/tdameritrade_api) 4 | 5 | This is a gem for connecting to the TD Ameritrade API. 6 | 7 | ## Installation 8 | 9 | Add this line to your application's Gemfile: 10 | 11 | gem 'tdameritrade_api', :git=>'https://github.com/wakproductions/tdameritrade_api.git' 12 | 13 | ## Important Note 14 | 15 | This is in the very early stages of development. It has very limited functionality in comparison to the entirety 16 | of the API. See the /vendor/docs folder for more details on the overall API. 17 | 18 | ## Setup 19 | 20 | To use this, you need to have 3 environment variables set: 21 | 22 | TDAMERITRADE_SOURCE_KEY - this is given to you by TD Ameritrade 23 | TDAMERITRADE_USER_ID - your username to connect to TD Ameritrade 24 | TDAMERITRADE_PASSWORD - your account password for TD Ameritrade 25 | 26 | 27 | ## Basic Usage 28 | 29 | To connect to the TD Ameritrade API using this gem, create an instance of TDAmeritradeApi::Client and then 30 | call the methods you need. 31 | 32 | c = TDAmeritradeApi::Client.new 33 | c.login 34 | #=> true 35 | 36 | c.get_price_history('MSFT', intervaltype: :minute, intervalduration: 15, periodtype: :day, period: 10) 37 | #=> [{:open=>41.75, :high=>41.87, :low=>41.71, :close=>41.85, :volume=>17955.3, :timestamp=>2014-07-07 38 | 09:30:00 -0400, :interval=>:day}, {:open=>41.85, :high=>41.92, :low=>41.84, :close=>41.9, 39 | :volume=>7380.78, :timestamp=>2014-07-07 09:45:00 -0400, :interval=>:day},... a long hash array of 40 | the price candles] 41 | 42 | ## Currently Supported Methods 43 | 44 | The only API features really supported right now are the ability to capture real time quotes, 45 | price history, and streaming of Level 1 quotes. 46 | 47 | login 48 | get_price_history # retrieves historical data 49 | get_quote # gets realtime snapshots of quotes 50 | 51 | ## Streaming 52 | 53 | c = TDAmeritradeApi::Client.new 54 | c.login 55 | streamer = c.create_streamer 56 | streamer.run(symbols: symbols, request_fields: [:volume, :last, :symbol, :quotetime, :tradetime]) do |data| 57 | # Process the stream data here - this block gets called for every new chunk of data received from TDA 58 | # See what's in the data hash to get the requested information streaming in about the stock 59 | end 60 | 61 | The streamer also has the ability to read and write from a hard disk file for testing: 62 | 63 | # This output_file attribute will cause the stream to be saved into a file as its being processed 64 | streamer.output_file = '/Users/wkotzan/Development/gem-development/tda_stream_daemon/cache/stream20150205.binary' 65 | 66 | # Run this code to read a stream from a presaved file 67 | input_file = '~/stream20150213-should-have-WUBA-1010am.binary' 68 | streamer = TDAmeritradeApi::Streamer::Streamer.new(read_from_file: input_file) 69 | 70 | ## Watchlists (see lib/watchlist.rb for accepted parameters) 71 | 72 | get_watchlists 73 | create_watchlist 74 | edit_watchlist 75 | 76 | ## Balances and Positions (see lib/balances_and_positions.rb for accepted parameters) 77 | 78 | get_balances_and_positions 79 | 80 | ## Contributions 81 | 82 | If you would like to make a contribution, please submit a pull request to the original branch. Feel free to email me Winston Kotzan 83 | at wak@wakproductions.com with any feature requests, bug reports, or feedback. 84 | 85 | 86 | ## Release Notes 87 | 88 | See CHANGELOG.md -------------------------------------------------------------------------------- /lib/tdameritrade_api/streamer_types.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/time' 2 | 3 | module TDAmeritradeApi 4 | module StreamerTypes 5 | SERVICE_ID={ 6 | quote: "1", 7 | timesale: "5", 8 | response: "10", 9 | option: "18", 10 | actives_nyse: "23", 11 | actives_nasdaq: "25", 12 | actives_otcbb: "26", 13 | actives_options: "35", 14 | news: "27", 15 | news_history: "28", 16 | adap_nasdaq: "62", 17 | nyse_book: "81", 18 | nyse_chart: "82", 19 | nasdaq_chart: "83", 20 | opra_book: "84", 21 | index_chart: "85", 22 | total_view: "87", 23 | acct_activity: "90", 24 | chart: "91", 25 | streamer_server: "100" 26 | } 27 | 28 | LEVEL1_COLUMN_NUMBER={ 29 | symbol: 0, 30 | bid: 1, 31 | ask: 2, 32 | last: 3, 33 | bidsize: 4, 34 | asksize: 5, 35 | bidid: 6, 36 | askid: 7, 37 | volume: 8, 38 | lastsize: 9, 39 | tradetime: 10, 40 | quotetime: 11, 41 | high: 12, 42 | low: 13, 43 | tick: 14, 44 | close: 15, 45 | exchange: 16, 46 | marginable: 17, 47 | shortable: 18, 48 | quotedate: 22, 49 | tradedate: 23, 50 | volatility: 24, 51 | description: 25, 52 | trade_id: 26, 53 | digits: 27, 54 | open: 28, 55 | change: 29, 56 | week_high_52: 30, 57 | week_low_52: 31, 58 | p_e_ratio: 32, 59 | dividend_amt: 33, 60 | dividend_yield: 34, 61 | nav: 37, 62 | fund: 38, 63 | exchange_name: 39, 64 | dividend_date: 40, 65 | last_market_hours: 41, 66 | lastsize_market_hours: 42, 67 | tradedate_market_hours: 43, 68 | tradetime_market_hours: 44, 69 | change_market_hours: 45, 70 | is_regular_market_quote: 46, 71 | is_regular_market_trade: 47, 72 | service_id: 100 73 | } 74 | 75 | LEVEL1_COLUMN_TYPE={ 76 | symbol: :string, 77 | bid: :float, 78 | ask: :float, 79 | last: :float, 80 | bidsize: :int, 81 | asksize: :int, 82 | bidid: :char, 83 | askid: :char, 84 | volume: :long, 85 | lastsize: :int, 86 | tradetime: :int, 87 | quotetime: :int, 88 | high: :float, 89 | low: :float, 90 | tick: :char, 91 | close: :float, 92 | exchange: :char, 93 | marginable: :boolean, 94 | shortable: :boolean, 95 | quotedate: :int, 96 | tradedate: :int, 97 | volatility: :float, 98 | description: :string, 99 | trade_id: :char, 100 | digits: :int, 101 | open: :float, 102 | change: :float, 103 | week_high_52: :float, 104 | week_low_52: :float, 105 | p_e_ratio: :float, 106 | dividend_amt: :float, 107 | dividend_yield: :float, 108 | nav: :float, 109 | fund: :float, 110 | exchange_name: :string, 111 | dividend_date: :string, 112 | last_market_hours: :float, 113 | lastsize_market_hours: :int, 114 | tradedate_market_hours: :int, 115 | tradetime_market_hours: :int, 116 | change_market_hours: :float, 117 | is_regular_market_quote: :boolean, 118 | is_regular_market_trade: :boolean, 119 | service_id: :short 120 | } 121 | 122 | STREAM_DATA_TYPE=[:heartbeat, :snapshot, :stream_data] 123 | class StreamData 124 | attr_accessor :stream_data_type, :timestamp_indicator, :timestamp, :service_id, :columns 125 | 126 | def initialize(stream_data_type) 127 | @stream_data_type=stream_data_type 128 | end 129 | 130 | def service_type 131 | SERVICE_ID.key(service_id) 132 | end 133 | 134 | def convert_time_columns(day=Date.today) 135 | return if @columns.nil? 136 | time_columns = [:tradetime, :quotetime] 137 | time_columns.each do |tc| 138 | if @columns.has_key? tc 139 | # TODO Investigate whether this still works if the computer is in a timezone other than ET 140 | @columns[(tc.to_s + '_ruby').to_sym] = Time.at(day.to_time.to_i + @columns[tc]) 141 | end 142 | end 143 | @columns 144 | end 145 | 146 | private 147 | # Because the time columns provided by the streamer are in # of seconds since midnight, Eastern Time 148 | def utc_seconds_conversion(time) 149 | time.in_time_zone('Eastern Time (US & Canada)').strftime('%z').to_i / 100 * -60 150 | end 151 | end 152 | end 153 | end -------------------------------------------------------------------------------- /vendor/docs/curl_test_strings.txt: -------------------------------------------------------------------------------- 1 | Some tests of the API using curl 2 | 3 | Login: 4 | curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d "userid=wkotzan&password=#password#&source=#source#&version=#version#" https://apis.tdameritrade.com/apps/100/LogIn?source=#source#&version=135 5 | 6 | 7 | 8 | 9 | curl -b "JSESSIONID=9D6D4732512988AB9BB9E0745D3097ED.OFzciMbhOJfVXUlMIniv3g" -d "source=___&requestidentifiertype=SYMBOL&requestvalue=KNDI&intervaltype=DAILY&intervalduration=1&startdate=20140711" https://apis.tdameritrade.com/apps/100/PriceHistory 10 | curl -b "JSESSIONID=___" https://apis.tdameritrade.com/apps/100/GetWatchlists?source=___ 11 | curl -b "JSESSIONID=___" https://apis.tdameritrade.com/apps/100/StreamerInfo?source= 12 | 13 | 14 | 15 | OK 16 | 17 | ameritrade02.streamer.com 18 | beefa8836348b812ec9b1cc723c5de69369e052e 19 | 1406514908163 20 | A000000025846220 21 | ACCT 22 | ACCT 23 | F7FUFXG1G3G5G7GKGRH1H3H5M1MANSOFPNRFSDSPTETFTOTSQ2NS 24 | WIKO 25 | Y 26 | 27 | 28 | 29 | 30 | curl -b "JSESSIONID=8E08C7AB68B89629514C7C88384E1C70.gAXa98X8axOJ9jeE6m9IqA" -d "!U=861214584&W=beefa8836348b812ec9b1cc723c5de69369e052e&A=userid=861214584&token=beefa8836348b812ec9b1cc723c5de69369e052e&company=AMER&segment=ADVNCED&cddomain=A000000025846220&usergroup=ACCT&accesslevel=ACCT&authorized=Y&acl=F7FUFXG1G3G5G7GKGRH1H3H5M1MANSOFPNRFSDSPTETFTOTSQ2NS×tamp=1406514908163&appid=WIKO|source=WIKO|control=false|version=1.0" http://ameritrade02.streamer.com/ 31 | curl -b "JSESSIONID=3933E5C53F00B3C117F478A388D5878F.gAXa98X8axOJ9jeE6m9IqA" -d "!U=861214584&W=15d5960722438430985947579c77c84129663075&A=userid=861214584&token=15d5960722438430985947579c77c84129663075&company=AMER&segment=ADVNCED&cddomain=A000000025846220&usergroup=ACCT&accesslevel=ACCT&authorized=Y&acl=F7FUFXG1G3G5G7GKGRH1H3H5M1MANSOFPNRFSDSPTETFTOTSQ2NS×tamp=1406820317416&appid=WIKO&source=WIKO&version=1.0&control=false&S=QUOTE&C=SUBS&P=VXX" http://ameritrade02.streamer.com/ 32 | 33 | 34 | !U=861214584&W=beefa8836348b812ec9b1cc723c5de69369e052e&A=userid=861214584&token=beefa8836348b812ec9b1cc723c5de69369e052e&company=AMER&segment=ADVNCED&cddomain=A000000025846220&usergroup=ACCT&accesslevel=ACCT&authorized=Y&acl=F7FUFXG1G3G5G7GKGRH1H3H5M1MANSOFPNRFSDSPTETFTOTSQ2NS×tamp=1406514908163&appid=WIKO|source=WIKO|control=false|version=1.0" 35 | 36 | OK8E08C7AB68B89629514C7C88384E1C70.gAXa98X8axOJ9jeE6m9IqAwkotzanA000000026237579552014-07-27 22:58:03 EDT861214584realtimerealtimerealtimerealtimedelayeddelayeddelayednon-professionalfalse861214584Swing TradeA000000025846220WINSTON A KOTZANtrueAMERADVNCEDtruefalsefalsefalsetruetruetruetruetruefulltruetrue861126628WAK IRAA000000025802215WINSTON A KOTZAN IRAfalseAMERADVNCEDtruefalsefalsefalsetruetruetruetruetruespreadtruetrue 37 | 38 | c.get_price_history('NEP', intervaltype: :minute, intervalduration: 5, startdate: Date.today-1, enddate: Date.today, extended: true) -------------------------------------------------------------------------------- /lib/tdameritrade_api/watchlist.rb: -------------------------------------------------------------------------------- 1 | require 'httparty' 2 | require 'nokogiri' 3 | require 'tdameritrade_api/constants' 4 | require 'tdameritrade_api/tdameritrade_api_error' 5 | 6 | module TDAmeritradeApi 7 | module Watchlist 8 | include Constants 9 | GET_WATCHLISTS_URL = 'https://apis.tdameritrade.com/apps/100/GetWatchlists' 10 | CREATE_WATCHLIST_URL = 'https://apis.tdameritrade.com/apps/100/CreateWatchlist' 11 | EDIT_WATCHLIST_URL = 'https://apis.tdameritrade.com/apps/100/EditWatchlist' 12 | 13 | # +get_watchlists+ allows you to retrieve watchlists for the associated account. Valid values for 14 | # opts are :accountid and :listid. See API docs for details. Returns an array of hashes, each 15 | # hash containing the :watchlist_name, and array of :ticker_symbols followed on the watchlist. 16 | # Note that as of now the data returned is not exactly in the same organization or level of detail 17 | # as the data returned by the TDA API. 18 | def get_watchlists(opts={}) 19 | request_params = { source: @source_id } 20 | request_params.merge(opts) # valid values are accountid and listid - see the API docs for details 21 | 22 | uri = URI.parse GET_WATCHLISTS_URL 23 | uri.query = URI.encode_www_form(request_params) 24 | 25 | response = HTTParty.get(uri, headers: {'Cookie' => "JSESSIONID=#{@session_id}"}, timeout: DEFAULT_TIMEOUT) 26 | if response.code != 200 27 | fail "HTTP response #{response.code}: #{response.body}" 28 | end 29 | 30 | watchlists = Array.new 31 | w = Nokogiri::XML::Document.parse response.body 32 | w.css('watchlist').each do |watchlist| 33 | watchlist_name = watchlist.css('name').text 34 | watchlist_id = watchlist.css('id').text 35 | watchlist_symbols = [] 36 | 37 | watchlist.css('watched-symbol').each do |ws| 38 | watchlist_symbols << ws.css('security symbol').text 39 | end 40 | 41 | watchlists << { name: watchlist_name, id: watchlist_id, symbols: watchlist_symbols } 42 | end 43 | 44 | watchlists 45 | 46 | rescue Exception => e 47 | raise TDAmeritradeApiError, "error retrieving watchlists - #{e.message}" if !e.is_ctrl_c_exception? 48 | end 49 | 50 | def create_watchlist(opts={}) 51 | # valid values are watchlistname and symbollist - see the API docs for details 52 | fail 'watchlistname required!' unless opts[:watchlistname] 53 | fail 'symbollist required! (at least 1 symbol)' unless opts[:symbollist] 54 | opts[:symbollist] = opts[:symbollist].join(',') if opts[:symbollist].is_a? Array 55 | #request_params = { source: @source_id }.merge(opts) #TODO write a method to build params using this 56 | 57 | uri = URI.encode( 58 | CREATE_WATCHLIST_URL << "?source=#{@source_id}&watchlistname=#{opts[:watchlistname]}&symbollist=#{opts[:symbollist]}" 59 | ) 60 | 61 | response = HTTParty.get(uri, headers: {'Cookie' => "JSESSIONID=#{@session_id}"}, timeout: DEFAULT_TIMEOUT) 62 | if response.code != 200 63 | fail "HTTP response #{response.code}: #{response.body}" 64 | end 65 | 66 | w = Nokogiri::XML::Document.parse response.body 67 | result = { 68 | result: w.css('result').text, 69 | error: w.css('error').text, 70 | account_id: w.css('account-id').text, 71 | watchlistname: w.css('created-watchlist name').text 72 | } 73 | watchlist = [] 74 | w.css('created-watchlist symbol-list watched-symbol').each do |ws| 75 | watchlist << { 76 | symbol: ws.css('symbol').text, 77 | symbol_with_type_prefix: ws.css('symbol-with-type-prefix').text, 78 | description: ws.css('description').text, 79 | asset_type: ws.css('asset-type').text 80 | } 81 | end 82 | result[:watchlist] = watchlist 83 | result 84 | rescue Exception => e 85 | raise TDAmeritradeApiError, e.message 86 | end 87 | 88 | def edit_watchlist(opts={}) 89 | # valid values are watchlistname and symbollist - see the API docs for details 90 | fail 'listid required!' unless opts[:listid] 91 | fail 'symbollist required! (at least 1 symbol)' unless opts[:symbollist] 92 | opts[:symbollist] = opts[:symbollist].join(',') if opts[:symbollist].is_a? Array 93 | #request_params = { source: @source_id }.merge(opts) #TODO write a method to build params using this 94 | 95 | query = { listid: opts[:listid], symbollist: opts[:symbollist] } 96 | uri = URI.encode( 97 | EDIT_WATCHLIST_URL << "?source=#{@source_id}" 98 | ) 99 | 100 | response = HTTParty.post( 101 | uri, 102 | headers: {'Cookie' => "JSESSIONID=#{@session_id}"}, 103 | query: query, 104 | timeout: DEFAULT_TIMEOUT 105 | ) 106 | if response.code != 200 107 | fail "HTTP response #{response.code}: #{response.body}" 108 | end 109 | 110 | w = Nokogiri::XML::Document.parse response.body 111 | result = { 112 | result: w.css('result').text, 113 | error: w.css('edit-watchlist-result error').text, 114 | account_id: w.css('edit-watchlist-result account-id').text, 115 | watchlistname: w.css('edited-watchlist name').text 116 | } 117 | watchlist = [] 118 | w.css('edited-watchlist symbol-list watched-symbol').each do |ws| 119 | watchlist << { 120 | symbol: ws.css('symbol').text, 121 | symbol_with_type_prefix: ws.css('symbol-with-type-prefix').text, 122 | description: ws.css('description').text, 123 | asset_type: ws.css('asset-type').text 124 | } 125 | end 126 | result[:watchlist] = watchlist 127 | result 128 | rescue Exception => e 129 | raise TDAmeritradeApiError, e.message 130 | end 131 | end 132 | end -------------------------------------------------------------------------------- /spec/tdameritrade_api/streamer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe TDAmeritradeApi::Client do 4 | let(:client) { RSpec.configuration.client } 5 | let(:streamer) {client.create_streamer} 6 | let(:watchlist) { load_watchlist } 7 | let(:mock_data_file) { File.join(Dir.pwd, 'spec', 'test_data', 'sample_stream_20140807.binary') } 8 | 9 | # These three below are settings you can change for working on the tests and experimenting with the gem 10 | let(:display_output) { false } 11 | let(:test_mock_data) { true } 12 | let(:connect_to_stream) { true } 13 | 14 | it 'should create a valid streamer' do 15 | expect(streamer.streamer_info_response).to be_a(String) 16 | expect(streamer.authentication_params).to be_a(Hash) 17 | end 18 | 19 | # This is intended to test the quote processing system by reading a previously saved stream of data. 20 | # It is more useful than connecting to the stream directly because you may not have much data coming in 21 | # depending on whether the market is open when you run the test against a live connection. 22 | # 23 | # You can turn off this test by setting the test_mock_data variable to false 24 | it 'should be able to process a complex stream saved to a file' do 25 | if test_mock_data 26 | #display_output_before = display_output 27 | pout "Testing Level 1 stream from mock data" 28 | first_heartbeat = nil 29 | first_stream_data = nil 30 | 31 | i = 1 32 | streamer = TDAmeritradeApi::Streamer::Streamer.new(read_from_file: mock_data_file) 33 | streamer.run do |data| 34 | data.convert_time_columns Date.new(2014,07,16) # Date that /spec/test_data/sample_stream_rspec_test.binary was created. Must update this date if you change the sample file bc of daylight savings time adjustment. 35 | case data.stream_data_type 36 | when :heartbeat 37 | pout "Heartbeat: #{data.timestamp}" 38 | first_heartbeat = data if first_heartbeat.nil? 39 | when :snapshot 40 | if data.service_id == "100" 41 | pout "Snapshot SID-#{data.service_id}: #{data.columns[:description]}" 42 | else 43 | pout "Snapshot: #{data}" 44 | end 45 | when :stream_data 46 | first_stream_data = data if first_stream_data.nil? 47 | cols = data.columns.each { |k,v| "#{k}: #{v} "} 48 | pout "Stream: #{cols}" 49 | else 50 | pout "Unknown type of data: #{data}" 51 | end 52 | i += 1 53 | streamer.quit if i == 15 54 | end 55 | end 56 | expect(first_heartbeat.timestamp).to eql(Time.parse("2014-07-16 09:49:34 -0400")) 57 | expect(first_stream_data.columns[:symbol]).to eql("ETN") 58 | expect(first_stream_data.columns[:bid]).to eql(47.76) 59 | expect(first_stream_data.columns[:ask]).to eql(47.78) 60 | expect(first_stream_data.columns[:last]).to eql(47.86) 61 | expect(first_stream_data.columns[:volume]).to eql(3424063) 62 | expect(first_stream_data.columns[:tradetime]).to eql(69439) 63 | expect(first_stream_data.columns[:quotetime]).to eql(72000) 64 | expect(first_stream_data.columns[:high]).to eql(47.8) 65 | expect(first_stream_data.columns[:tick]).to eql("\x00") 66 | expect(first_stream_data.columns[:low]).to eql(47.77) 67 | expect(first_stream_data.columns[:close]).to eql(47.81) 68 | 69 | # Note - Time zones may not reflect correctly if you run this test in a time zone other than US/Eastern 70 | # To fix this we will need to write a conversion to make StreamerTypes::StreamData.convert_time_columns adjust 71 | # local time to eastern time. It gets 72 | #expect(first_stream_data.columns[:tradetime_ruby]).to eql(Time.parse("2014-07-16 19:17:19 -0400")) # This test was problematic due to time zone 73 | #expect(first_stream_data.columns[:quotetime_ruby]).to eql(Time.parse("2014-07-16 20:00:00 -0400")) 74 | 75 | end 76 | 77 | 78 | # This tests the behavior of a successful connection and download of streaming data from TDA. 79 | # The way this test works is it connects to the API streaming server for 15 seconds, 80 | # then it checks for the following conditions: 81 | # 1) a success message was received 82 | # 2) a heartbeat message was received 83 | # 3) a streaming quote message was received 84 | # 4) the stream was saved to a file 85 | # 5) any unexpected exception thrown will caused the Rspec test to fail 86 | # 87 | # Note that this test is skipped if connect_to_stream is false. 88 | it "downloads and processes streaming data from TDA and saves the stream to a file" do 89 | 90 | if connect_to_stream 91 | pout "Testing TD Ameritrade Level 1 quote data stream" 92 | 93 | # Test conditions that will be checked later 94 | has_heartbeat_message = false 95 | has_successful_connect_message = false 96 | has_quote_stream_message = false 97 | 98 | request_fields = [:volume, :last, :bid, :symbol, :ask, :quotetime, :high, :low, :close, :tradetime, :tick] 99 | symbols = watchlist 100 | file_name = File.join(Dir.pwd, 'spec', 'test_data', 'sample_stream_rspec_test.binary') 101 | i = 1 102 | 103 | File.delete(file_name) if File.exists? file_name 104 | 105 | streamer.output_file = file_name 106 | streamer.run(symbols: symbols, request_fields: request_fields) do |data| 107 | data.convert_time_columns 108 | case data.stream_data_type 109 | when :heartbeat 110 | pout "Heartbeat: #{data.timestamp}" 111 | has_heartbeat_message = true 112 | when :snapshot 113 | if data.service_id == "100" 114 | pout "Snapshot SID-#{data.service_id}: #{data.columns[:description]}" 115 | has_successful_connect_message = true if data.columns[:description]=='SUCCESS' && data.columns[:return_code]==0 116 | else 117 | pout "Snapshot: #{data}" 118 | end 119 | when :stream_data 120 | pout "#{i} Stream: #{data.columns}" 121 | has_quote_stream_message = true 122 | else 123 | pout "Unknown type of data: #{data}" 124 | end 125 | i += 1 126 | streamer.quit if i > 5 # We only need a few records 127 | end 128 | 129 | expect(has_heartbeat_message).to be_truthy 130 | expect(has_successful_connect_message).to be_truthy 131 | expect(has_quote_stream_message).to be_truthy 132 | expect(File.exists?(file_name)).to be_truthy 133 | expect(File.size(file_name)).to be > 10 # should have more than 10 bytes of data (arbitrary small number) 134 | else 135 | # this is only if we are skipping the test 136 | expect(true).to be_truthy 137 | end 138 | 139 | 140 | end 141 | 142 | private 143 | def pout(output) 144 | puts output if display_output 145 | end 146 | 147 | def load_watchlist 148 | wl_file = File.join(Dir.pwd, 'spec', 'test_data', 'watchlist.txt') 149 | f = File.open(wl_file, 'r') 150 | list = f.read().split("\n") 151 | f.close 152 | list 153 | end 154 | 155 | def new_mock_data_file_name(i) 156 | "sample_stream_#{Date.today.strftime('%Y%m%d')}-0#{i}.binary" 157 | end 158 | end -------------------------------------------------------------------------------- /lib/tdameritrade_api/price_history.rb: -------------------------------------------------------------------------------- 1 | require 'tmpdir' 2 | 3 | module TDAmeritradeApi 4 | module PriceHistory 5 | include BinDataTypes 6 | 7 | PRICE_HISTORY_URL='https://apis.tdameritrade.com/apps/100/PriceHistory' 8 | INTERVAL_TYPE=[:minute, :daily, :weekly, :monthly] 9 | PERIOD_TYPE=[:day, :month, :year, :ytd] 10 | 11 | # +get_price_history+ allows you to send a price history request. For now it can only accommodate one 12 | # symbol at a time. +options+ may contain any of the following params outlined in the API docs 13 | # * periodtype: (:day, :month, :year, :ytd) 14 | # * period: number of periods for which data is returned 15 | # * intervaltype (:minute, :daily, :weekly, :monthly) 16 | # * intervalduration 17 | # * startdate 18 | # * enddate 19 | # * extended: true/false 20 | def get_price_history(symbol, options={}) 21 | # TODO: allow multiple symbols by allowing user to pass and array of strings 22 | # TODO: change this around so that it does not need a temporary file buffer and can handle the processing in memory 23 | validate_price_history_options options 24 | request_params = build_price_history_request_params(symbol, options) 25 | 26 | uri = URI.parse PRICE_HISTORY_URL 27 | uri.query = URI.encode_www_form(request_params) 28 | 29 | response = HTTParty.get(uri, headers: {'Cookie' => "JSESSIONID=#{@session_id}"}, timeout: 10) 30 | if response.code != 200 31 | raise TDAmeritradeApiError, "HTTP response #{response.code}: #{response.body}" 32 | end 33 | 34 | tmp_file=File.join(Dir.tmpdir, "daily_prices.binary") 35 | w = open(tmp_file, 'wb') 36 | w.write(response.body) 37 | w.close 38 | 39 | rd = open(tmp_file, 'rb') 40 | 41 | result = Array.new 42 | header = PriceHistoryHeader.read(rd) 43 | header.symbol_count.times do |count| 44 | symbol_data_raw = PriceHistorySymbolData.read(rd) 45 | symbol_data = { symbol: symbol_data_raw.symbol } 46 | 47 | if symbol_data_raw.error_code == 0 48 | prices = Array.new 49 | while rd.read(2).bytes != [255,255] # The terminator char is "\xFF\xFF" 50 | rd.seek(-2, IO::SEEK_CUR) 51 | bar = PriceHistoryBarRaw.read(rd) 52 | prices << { 53 | open: bar.open.round(2), 54 | high: bar.high.round(2), 55 | low: bar.low.round(2), 56 | close: bar.close.round(2), 57 | volume: bar.volume.round(2), # volume is presented in 100's, per TD Ameritrade API spec 58 | timestamp: Time.at(bar.timestampint/1000), 59 | interval: :day 60 | } 61 | #puts "#{bar.open} #{bar.high} #{bar.low} #{bar.close} #{Time.at(bar.timestampint/1000)}" 62 | end 63 | symbol_data[:bars] = prices 64 | 65 | else 66 | symbol_data[:error_code] = symbol_data_raw.error_code 67 | symbol_data[:error_text] = symbol_data_raw.error_text 68 | end 69 | 70 | result << symbol_data 71 | end 72 | result 73 | rescue Exception => e 74 | raise TDAmeritradeApiError, "error in get_price_history() - #{e.message}" if !e.is_ctrl_c_exception? 75 | end 76 | 77 | # +get_daily_price_history+ is a shortcut for +get_price_history()+ for getting a series of daily price candles 78 | # It adds convenience because you can just specify a begin_date and end_date rather than all of the 79 | # TDAmeritrade API parameters. 80 | def get_daily_price_history(symbol, start_date=Date.new(2001,1,2), end_date=todays_date) 81 | get_price_history(symbol, intervaltype: :daily, intervalduration: 1, startdate: start_date, enddate: end_date).first[:bars] 82 | end 83 | 84 | # this currently only works on stocks 85 | def get_quote(symbols) 86 | if symbols.is_a? Array 87 | quote_list = symbols.join(',') if symbols.is_a? Array 88 | else 89 | quote_list=symbols 90 | end 91 | 92 | uri = URI.parse("https://apis.tdameritrade.com/apps/100/Quote;jsessionid=#{@session_id}?source=#{@source_id}&symbol=#{quote_list}") 93 | http = Net::HTTP.new(uri.host, uri.port) 94 | http.use_ssl = true 95 | request = Net::HTTP::Get.new uri 96 | request['Set-Cookie'] = "JSESSIONID=#{@session_id}" 97 | 98 | begin 99 | response = http.request request 100 | rescue 101 | # TODO set this up to re-raise the exception 102 | puts "error here in api- get_quote function" 103 | end 104 | #puts response.body 105 | 106 | quotes = Array.new 107 | q = Nokogiri::XML::Document.parse response.body 108 | q.css('quote').each do |q| 109 | quotes << { 110 | error: q.css('error').text, 111 | symbol: q.css('symbol').text, 112 | description: q.css('description').text, 113 | bid: q.css('bid').text, 114 | ask: q.css('ask').text, 115 | bid_ask_size: q.css('bid-ask-size').text, 116 | last: q.css('last').text, 117 | last_trade_size: q.css('last-trade-size').text, 118 | last_trade_time: parse_last_trade_date(q.css('last-trade-date').text), 119 | open: q.css('open').text, 120 | high: q.css('high').text, 121 | low: q.css('low').text, 122 | close: q.css('close').text, 123 | volume: q.css('volume').text, 124 | year_high: q.css('year-high').text, 125 | year_low: q.css('year-low').text, 126 | real_time: q.css('real-time').text, 127 | exchange: q.css('exchange').text, 128 | asset_type: q.css('asset-type').text, 129 | change: q.css('change').text, 130 | change_percent: q.css('change-percent').text 131 | } 132 | end 133 | 134 | quotes 135 | end 136 | 137 | private 138 | 139 | def todays_date 140 | Date.today 141 | end 142 | 143 | def parse_last_trade_date(date_string) 144 | DateTime.parse(date_string) 145 | rescue 146 | 0 147 | end 148 | 149 | def date_s(date) 150 | date.strftime('%Y%m%d') 151 | end 152 | 153 | def validate_price_history_options(options) 154 | if options.has_key?(:intervaltype) && INTERVAL_TYPE.index(options[:intervaltype]).nil? 155 | raise TDAmeritradeApiError, "Invalid price history option for intervaltype: #{options[:intervaltype]}" 156 | end 157 | 158 | if options.has_key?(:periodtype) && PERIOD_TYPE.index(options[:periodtype]).nil? 159 | raise TDAmeritradeApiError, "Invalid price history option for periodtype: #{options[:periodtype]}" 160 | end 161 | 162 | end 163 | 164 | def build_price_history_request_params(symbol, options) 165 | req = {source: @source_id, requestidentifiertype: 'SYMBOL'}.merge(options) 166 | 167 | if symbol.kind_of? String 168 | req[:requestvalue] = symbol 169 | elsif symbol.kind_of? Array 170 | req[:requestvalue] = symbol.inject { |symbol, join| join = join + ", #{symbol}" } 171 | end 172 | 173 | req[:startdate]=date_s(req[:startdate]) if req.has_key?(:startdate) && req[:startdate].is_a?(Date) 174 | req[:enddate]=date_s(req[:enddate]) if req.has_key?(:enddate) && req[:enddate].is_a?(Date) 175 | req[:intervaltype]=req[:intervaltype].to_s.upcase if req[:intervaltype] 176 | req[:periodtype]=req[:periodtype].to_s.upcase 177 | req 178 | end 179 | end 180 | end -------------------------------------------------------------------------------- /lib/tdameritrade_api/streamer.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'tdameritrade_api/streamer_types' 3 | 4 | module TDAmeritradeApi 5 | module Streamer 6 | 7 | # +create_streamer+ use this to create a connection to the TDA streaming server 8 | def create_streamer 9 | Streamer.new(streamer_info_raw: get_streamer_info, login_params: login_params_hash, session_id: @session_id) 10 | end 11 | 12 | class Streamer 13 | include StreamerTypes 14 | STREAMER_REQUEST_URL='http://ameritrade02.streamer.com/' 15 | 16 | attr_accessor :output_file 17 | attr_reader :streamer_info_response, :authentication_params, :session_id, :symbols, :request_fields 18 | 19 | def initialize(opt={}) 20 | if opt.has_key? :read_from_file 21 | @read_from_file = opt[:read_from_file] # if this option is used, it will read the stream from a saved file instead of connecting 22 | else 23 | @streamer_info_response = opt[:streamer_info_raw] 24 | @session_id = opt[:session_id] 25 | 26 | @authentication_params = Hash.new 27 | @authentication_params = @authentication_params.merge(opt[:login_params]).merge(parse_streamer_request_params) 28 | end 29 | 30 | @buffer = String.new 31 | @message_block = nil 32 | @quit = false 33 | end 34 | 35 | def run(opt={}, &block) 36 | @quit = false 37 | @message_block = block 38 | @buffer = String.new 39 | 40 | if !@read_from_file.nil? 41 | run_from_file 42 | return 43 | end 44 | 45 | if !opt.has_key?(:symbols) && !opt.has_key?(:request_fields) 46 | raise TDAmeritradeApiError, ":symbols and :request_fields are required parameters for Streamer.run()" 47 | end 48 | @symbols = opt[:symbols] 49 | @request_fields = opt[:request_fields] 50 | symbol_list = process_symbols(opt[:symbols]) 51 | request_fields_list = process_request_fields(opt[:request_fields]) 52 | 53 | uri = URI.parse STREAMER_REQUEST_URL 54 | post_data="!U=#{authentication_params[:account_id]}&W=#{authentication_params[:token]}&" + 55 | "A=userid=#{authentication_params[:account_id]}&token=#{authentication_params[:token]}&" + 56 | "company=#{authentication_params[:company]}&segment=#{authentication_params[:segment]}&" + 57 | "cddomain=#{authentication_params[:cd_domain_id]}&usergroup=#{authentication_params[:usergroup]}&" + 58 | "accesslevel=#{authentication_params[:access_level]}&authorized=#{authentication_params[:authorized]}&" + 59 | "acl=#{authentication_params[:acl]}×tamp=#{authentication_params[:timestamp]}&" + 60 | "appid=#{authentication_params[:app_id]}|S=QUOTE&C=SUBS&P=#{symbol_list}&T=#{request_fields_list}|control=false" + 61 | "|source=#{authentication_params[:source]}\n\n" 62 | 63 | request = Net::HTTP::Post.new('/') 64 | request.body = post_data 65 | 66 | # clear the output file 67 | if @output_file 68 | File.delete(@output_file) if File.exists?(@output_file) 69 | end 70 | 71 | Net::HTTP.start(uri.host, uri.port) do |http| 72 | http.request(request) do |response| 73 | if !@quit 74 | response.read_body do |chunk| # right here is the connection reset error 75 | if !@quit 76 | @buffer = @buffer + chunk 77 | save_to_output_file(chunk) if @output_file 78 | process_buffer 79 | else 80 | http.finish 81 | return 82 | end 83 | end 84 | else 85 | http.finish 86 | end 87 | end 88 | end 89 | 90 | end 91 | 92 | def quit 93 | @quit = true 94 | end 95 | 96 | private 97 | 98 | def save_to_output_file(chunk) 99 | w = File.open(@output_file, 'ab') 100 | w.write(chunk.b) 101 | w.close 102 | end 103 | 104 | def post_data(data) 105 | @message_block.call(data) # sends formatted stream data back to block passed to Streamer.run 106 | end 107 | 108 | def run_from_file 109 | @quit = false 110 | r = open(@read_from_file, 'rb') 111 | while (data=r.read(1000)) && !@quit 112 | @buffer = @buffer + data 113 | process_buffer 114 | end 115 | end 116 | 117 | def build_parameters(opts={}) 118 | { 119 | "!U"=>authentication_params[:account_id], 120 | "W"=>authentication_params[:token], 121 | "A=userid"=>authentication_params[:account_id], 122 | "token"=>authentication_params[:token], 123 | "company"=>authentication_params[:company], 124 | "segment"=>authentication_params[:segment], 125 | "cddomain"=>authentication_params[:cd_domain_id], 126 | "usergroup"=>authentication_params[:usergroup], 127 | "accesslevel"=>authentication_params[:access_level], 128 | "authorized"=>authentication_params[:authorized], 129 | "acl"=>authentication_params[:acl], 130 | "timestamp"=>authentication_params[:timestamp], 131 | "appid"=>authentication_params[:app_id], 132 | "source"=>authentication_params[:source], 133 | "version"=>"1.0" 134 | }.merge(opts) 135 | end 136 | 137 | def parse_streamer_request_params 138 | p = Hash.new 139 | r = Nokogiri::XML::Document.parse @streamer_info_response 140 | si = r.xpath('/amtd/streamer-info').first 141 | p[:token] = si.xpath('token').text 142 | p[:cd_domain_id] = si.xpath('cd-domain-id').text 143 | p[:usergroup] = si.xpath('usergroup').text 144 | p[:access_level] = si.xpath('access-level').text 145 | p[:acl] = si.xpath('acl').text 146 | p[:app_id] = si.xpath('app-id').text 147 | p[:authorized] = si.xpath('authorized').text 148 | p[:timestamp] = si.xpath('timestamp').text 149 | p 150 | end 151 | 152 | def process_symbols(symbols) 153 | # symbols should be an array of strings 154 | symbols.join('+') 155 | end 156 | 157 | def process_request_fields(fields) 158 | fields.map { |c| LEVEL1_COLUMN_NUMBER[c] }.sort.map { |c| c.to_s }.join('+') 159 | end 160 | 161 | def next_record_type_in_buffer 162 | if @buffer.length > 0 163 | case @buffer[0] 164 | when 'H' 165 | return :heartbeat 166 | when 'N' 167 | return :snapshot 168 | when 'S' 169 | return :stream_data 170 | else 171 | return nil 172 | end 173 | else 174 | return nil 175 | end 176 | end 177 | 178 | def process_heartbeat 179 | return false if @buffer.length < 2 180 | 181 | if @buffer[0] == 'H' 182 | hb = StreamData.new(:heartbeat) 183 | 184 | # Next char is 'T' (followed by time stamp) or 'H' (no time stamp) 185 | if @buffer[1] == 'T' 186 | return false if @buffer.length < 10 187 | hb.timestamp_indicator = true 188 | hb.timestamp = Time.at(@buffer[2..9].reverse.unpack('q').first/1000) 189 | @buffer.slice!(0, 10) 190 | elsif @buffer[1] != 'H' 191 | hb.timestamp_indicator = false 192 | @buffer.slice!(0, 2) 193 | else 194 | raise TDAmeritradeApiError, "Unexpected character in stream. Expected: Heartbeat timestamp indicator 'T' or 'H'" 195 | end 196 | 197 | post_data(hb) 198 | true 199 | end 200 | 201 | end 202 | 203 | def process_snapshot 204 | return false if @buffer[0] != 'N' || @buffer.length < 3 205 | 206 | n = StreamData.new(:snapshot) 207 | service_id_length = @buffer[1..2].reverse.unpack('S').first 208 | return false if @buffer.length < 3 + service_id_length 209 | n.service_id = @buffer.slice(3, service_id_length) 210 | 211 | case n.service_id 212 | when "100" # message from the server 213 | # next field will be the message length (4 bytes) followed by the message 214 | message_length = @buffer.slice(3 + service_id_length, 4)[0..3].reverse.unpack('L').first 215 | message_bytes = @buffer.slice(3 + service_id_length + 4, message_length + 2) 216 | 217 | return false if @buffer.length < 3 + service_id_length + message_length + 2 218 | 219 | columns = Hash.new 220 | columns[:service_id] = message_bytes.slice(3, 2).reverse.unpack('S').first 221 | columns[:return_code] = message_bytes.slice(6, 2).reverse.unpack('S').first 222 | 223 | description_length = message_bytes.slice(9, 2).reverse.unpack('S').first 224 | columns[:description] = message_bytes.slice(11, description_length) 225 | n.columns = columns 226 | 227 | @buffer.slice!(0, 3 + service_id_length + 4 + message_length + 2) 228 | else 229 | n.message = "'N' Snapshot found (unsupported type): #{n.service_id}" 230 | end 231 | post_data(n) 232 | true 233 | end 234 | 235 | def process_stream_record 236 | return false if @buffer[0] != 'S' || @buffer.length < 3 237 | 238 | s = StreamData.new(:stream_data) 239 | columns = Hash.new 240 | 241 | message_length = @buffer[1..2].reverse.unpack('S').first 242 | return false if @buffer.length < message_length + 5 243 | data = @buffer.slice!(0, message_length + 5) # extract the entire message from the buffer 244 | data.slice!(0, 3) # chop off the 'S' flag and the message length 245 | 246 | s.service_id = data[0..1].reverse.unpack('S').first.to_s # I know, the API is inconsistent in its use of string vs integer for SID 247 | data.slice!(0, 2) 248 | 249 | until data.length <= 2 # last two characters should be the delimiters 0xFF,0x0A 250 | column_number = data[0].unpack('c').first 251 | column_name = LEVEL1_COLUMN_NUMBER.key(column_number) 252 | column_type = LEVEL1_COLUMN_TYPE[column_name] 253 | column_value = read_stream_column(data, column_type) 254 | columns[column_name] = column_value 255 | end 256 | 257 | s.columns = columns 258 | 259 | post_data(s) 260 | true 261 | end 262 | 263 | def read_stream_column(data, column_type) 264 | 265 | # First byte of data should still contain the column number 266 | case column_type 267 | when :string 268 | column_size = data[1..2].reverse.unpack('S').first 269 | column_value = data.slice(3, column_size) 270 | data.slice!(0, 3 + column_size) 271 | when :float 272 | column_value = data[1..4].reverse.unpack('F').first 273 | column_value = column_value.round(2) if column_value 274 | data.slice!(0, 5) 275 | when :int 276 | column_value = data[1..4].reverse.unpack('L').first 277 | data.slice!(0, 5) 278 | when :char 279 | column_value = data[1] 280 | data.slice!(0, 3) 281 | when :long 282 | column_value = data[1..8].reverse.unpack('Q').first 283 | data.slice!(0, 9) 284 | when :short 285 | column_value = data[1..2].reverse.unpack('S').first 286 | data.slice!(0, 3) 287 | when :boolean 288 | column_value = data.bytes[1] > 0 289 | data.slice!(0, 2) 290 | end 291 | column_value 292 | end 293 | 294 | def process_buffer 295 | @buffer = String.new and return if @quit # empty buffer and stop processing 296 | 297 | # advance until we get a recognizable code in the stream 298 | until @buffer.length == 0 || !next_record_type_in_buffer.nil? 299 | @buffer.slice!(0,1) 300 | end 301 | 302 | process_next = true 303 | until (process_next == false) || (next_record_type_in_buffer.nil?) || @quit 304 | case next_record_type_in_buffer 305 | when :heartbeat 306 | process_next = process_heartbeat 307 | when :snapshot 308 | process_next = process_snapshot 309 | when :stream_data 310 | process_next = process_stream_record 311 | end 312 | end 313 | end 314 | 315 | end 316 | 317 | private 318 | 319 | STREAMER_INFO_URL='https://apis.tdameritrade.com/apps/100/StreamerInfo' 320 | 321 | def get_streamer_info 322 | uri = URI.parse STREAMER_INFO_URL 323 | uri.query = URI.encode_www_form({source: @source_id}) 324 | 325 | response = HTTParty.get(uri, headers: {'Cookie' => "JSESSIONID=#{@session_id}"}, timeout: 10) 326 | if response.code != 200 327 | raise TDAmeritradeApiError, "HTTP response #{response.code}: #{response.body}" 328 | end 329 | 330 | response.body 331 | end 332 | 333 | def login_params_hash 334 | { 335 | company: @accounts.first[:company], 336 | segment: @accounts.first[:segment], 337 | account_id: @accounts.first[:account_id], 338 | source: @source_id 339 | } 340 | end 341 | end 342 | end -------------------------------------------------------------------------------- /spec/test_data/sample_stream_20140807.binary: -------------------------------------------------------------------------------- 1 | HTG?os?N100 2 | d01-3? 3 | N100 4 | d? 5 | N100 6 | dSUCCESS? 7 | S<ETNB? 8 | =B??B?p?4?? 9 | ? @ B?33 B?{ B??? 10 | S<CLXB?Q?B???B?z? ?l 11 | ?? ?? B??{ B?=q B?8R? 12 | S=ICPTC]Cc?C`?$t 13 | ?? @ Cb?3 C\E C_z?? 14 | S7WAGBvz?Bsz?b 15 | ) @ Bt?R Bk?R Bl? 16 | ? 17 | S;TMB?k?B?B?\)?W 18 | ?& @ B?(? B??) B?8R? 19 | S<MONB?B?33B?L?4?? 20 | ?? @ B?k? B??) B??? 21 | S<TXNB6B9ffB6?f? 22 | ?? @ B: B6ff B8?? 23 | S=REPH@?z?A ??@?=q ? 24 | ?? @ @?? @??? @?{? 25 | S<NSCB??)B??fB??? 26 | ?? @ B?33 B?? B?.? 27 | S=GIMOA2?HA=?A5G??? 28 | ?? ?? A=?< A4?? A<? 29 | S;BXB33BffB?T?? 30 |  @ B 31 | = B? Bz?? 32 | S=PVTBA??\A?G?A?z?? 33 | ?c ?? A?ff A??H A?? 34 | S=SWIRA?ffA?\)A?ff 35 | ?? 36 | ??  ? A??? A?z? A?ff? 37 | S=DNKNB*B1(?B-?H qa 38 | ?? @ B033 B-? B.? 39 | S=SONCA?A?A?z?> 40 | ?b ?_ A?=q A??R A?p?? 41 | S<ADMB8??BB??BB{#8 42 | ?s @ BC?1 BA?R BB?\? 43 | S<JCPA??A 44 | =A33??? 45 | 5 @ AQ? Az? A?? 46 | S=SCTYB???B?ffB???z?b 47 | < @ B??? B? ? B??R? 48 | S;BKBB1B??y? 49 | ?? @ B?? BQ? BQ?? 50 | S=INVNA?33A??RA?G?f6 51 | ?O ? A?? A?z? A?\)? 52 | S=DANGAq??Ar?\Aq??7?? 53 |  54 | K ? AuG? Ai?? Ae??? 55 | S;BAB?B?B??F?i 56 |  ? @ B?p? B??q B??? 57 | S8IBKRApA?33 ? 58 | ?b @ A??? A??\ A? 59 | =? 60 | S=DRYS@2?\@5??@2?\Cr? 61 | ?? ?? @;? @2?\ @6ff? 62 | S=GTATAs33Aw? 63 | As\)?3? 64 |  @ A| Ao 65 | = Ap{? 66 | S<ACNB?#?B?B? ?? 67 | ?? @ B?. B?? B??? 68 | S=GILDB?33B?W 69 | B?W 70 | ?~? 71 | ? @ B?I? B?Q? B??{? 72 | S=WYNNCB?fCC?fCC335w 73 | \ @ CJ?\ CBC? CH?=? 74 | S<ACEB?ffB???B???"? 75 | ?? S B?{ B?aH B?G?? 76 | S=QTWW@?@???@??? 77 | ? ? @?{ @? @?ff? 78 | S=JASOAffA?A??? 79 | ? ? A? A z? A ??? 80 | S<KEYAO\)AO?AO? 81 | a? 82 | ?? ? ATz? AO? AS33? 83 | S8TWTRB+33B,?h7 84 | 9 @ B1z? B+?\ B-? 85 | ? 86 | S=PAYXB {B%33B!??t 87 | ?? @ B#?\ B!{ B"?? 88 | S=ARUNA?A?? 89 | A?Q??` 90 | ?? ?{ A?G? A?ff A?? 91 | S=AVAVA?A???A??H?? 92 | ?c @ A? A??! A?{? 93 | S=INTUB?B???B?? 7? 94 | ?? 4 B?? B?(? B???? 95 | S<IOCBe\)Bo??Be\)?N 96 | ?{ ?G Bjff Bd{ Bgff? 97 | S=ENDPBHB?Q?Bv?W? 98 | ?U ? B~?? Bvff Bx? 99 | S=SBNYB?W 100 | B?B?.\? 101 | ?U R B?? B?W 102 |  B???? 103 | S<TDCB p?B/??B&z?O?^ 104 | ?? ? B3?? B$?H B,33? 105 | S<LZBA?z?A?? 106 | A???; 107 | ?? ? A?? A??? A???? 108 | S=BEAT@?A33@???z 109 | ?? R @??? @??? @???? 110 | S7INOA??A? mz 111 |  @ A{ A\) A?\? 112 | S=AMATA?A?A????a? 113 | & @ A?? A??? A?33? 114 | S=NYCBAtA}??Aw33?Y 115 | ?? ? Az=q Av=q AyG?? 116 | S=QLIKA???A?{A?? 117 | ?? @ A?dZ A??? A???? 118 | S=NTAPB33B?RB??1$ 119 | ?? @ Bp? B B33? 120 | S=CTRPB???B??B?ffl? 121 | ?< @ B??? B??R Bq??? 122 | S=EGLE?fff?? 123 | =??{? 124 | ? @ ???? ???? ????? 125 | S<TCSA?Q?A?A????| 126 | ?? ? A??? A?p? A?(?? 127 | S=SYNAB?B???B?\?l 128 | ?c @ B??{ B??? B???? 129 | S=ADBEB?B?B???"?( 130 | ?? @ B??N B?#? B?u?? 131 | S<AALB??B? 132 | B????) 133 | ; ; B?? B B? 134 | ? 135 | S=ADSKBLBY33BV?\? 136 | ?; @ B[=q BU?? BX??? 137 | S=GOOGD ??D ?D ??? 138 | ? ? Dx? D Ff D ??? 139 | S SPX ? 140 | S;PMB? 141 | =B???B?k?C?? 142 | t @ B??? B??? B???? 143 | S<OASB4BBB9??6?W 144 | ?? @ B?=q B7?? BB(p??k 185 | ?b S B*Q? B'?\ B(?? 186 | S8LULUB.G?B%??*?? 187 |  @ B!?? B?? B?H? 188 | S<BTUAz?\A}?Az?\L?? 189 | ?? @ A~?H Ax?? A{\)? 190 | S=ATHNB???C?fB?\)? 191 | ??  Q B??R B? B???? 192 | S=GOMOA A???A%??f 193 | ? @ A'V A$(? A$? 194 | S=ATHMB?B&??B?R?? 195 | ?? @ BT{ B  B?? 196 | S8YELPB?B?=q9?R 197 |  @ B??? B? B??\? 198 | S<GDPA?{A?{A?;F5 199 | ? @ A??? A? A??\? 200 | S<KSSBX?B^ffBY??51? 201 | ?? @ B] BY? BZ??? 202 | S=PCLND?`D??D??? 203 | Vj 204 | ? @ D??) D??? D?=? 205 | S=MOBI@???@?@???!? 206 | ?? @ @??? @?M @?33? 207 | S7WLPB?B??{"F6 208 | = @ B??R B?#? B??? 209 | S<BBYA?A?z?A?=q)? 210 | ? ? A?\) A??? A??\? 211 | S<BBTBz?B?B? 212 | ,*? 213 | ??  ? B B?\ B?? 214 | S=FUELA?A???A???!?W 215 | ? ? A? 216 | = A?z? A?ff? 217 | S=RRGBB|B?8RB}?H?P 218 | ?c ?N B?? Bz=q B|Q?? 219 | S<APAB?\)B?(?B?#?,PS 220 | D @ B?Q? B??? B??f? 221 | S<LVSB?p?B???B?u?? 222 |  @ B??3 B??? B?aH? 223 | S=SNDKB?\)B?B??fAJ 224 | W @ B??? B?O\ B?\? 225 | S=HZNPAz?A??Az?~4 226 | h @ A$ A? A?? 227 | S=PBYICV??Cl??Ck??# 228 | ?0 @ Cn?? Cd^? Ce\? 229 | S=ORCLB=qB?RB?? 230 | 231 |  @ B!?R BW 232 |  B ??? 233 | S;MY@%?@9??@(?? U 234 | ?? ? @1?? @(?? @/\)? 235 | S<DISB?33B?\)B?x?= 236 | ? @ B? B?u? B?.? 237 | S=RIGL@:=q@???@C33? 238 | ?? @ @N{ @C33 @K?? 239 | S<RJFB>p?B???BH 240 | =?n 241 | ?? S BKk? BGff BJ? 242 | S6MUA???A?ff%? 243 |  @ A?? A?% A?p?? 244 | S=WWAVBB?B\)_r? 245 | ?? @ B?? A??? A?p?? 246 | S<GCIB33B =qB??j? 247 | ??  B p? BW 248 |  B??? 249 | S;MOB$??B$??B$??ws? 250 | t ?? B&h B#?? B%?R? 251 | S<EMCA??\A?33A?G???N 252 | ?? @ A? A?ff A?G?? 253 | S=RFMDA0A2{A1???#7 254 | ? @ A7 255 | = A/ 256 | = A5p?? 257 | S<PBIA?33A?A??.?? 258 | ?s @ A?\) A? 259 | = A?ff? 260 | S<CVXB???B???B?L?F?? 261 | ?? @ B?u? B??? B?u?? 262 | S<CVSB?B?L?B?8RVs? 263 | & @ B?B? B?B? B?\? 264 | S7VXXB 265 | {B 266 | z?5*? 267 | ; @ B B+? B??? 268 | S=ESRXB???B???B?\FI 269 | ?? @ B??f B?W 270 |  B?? 271 | S=CALLAS?Ag33AT(?? 272 | ?? @ AdQ? AS? Ab?H? 273 | S;MAB??\B?B??G?? 274 | ? @ B?\) B??? B??R? 275 | S7MYLB7=qB:?? 276 | ? @ B?G? B5p? B??? 277 | S<UTXB?B???B?p??i4 278 | M @ B?#? B?z? B??{? 279 | S<PAYA?B 280 | B=q&? 281 | ?? @ B?\ B?? BQ?? 282 | S=OXGN???@ p?@ ? 283 | 2? 284 | ? @ @?\ @ 285 | =q @?\? 286 | S=GMCRB?L?B?aHB??G+) 287 | ? @ B??H B?\ B?D?? 288 | S8SIALB?aHB?8R?? 289 | ?? S B??? B??? B?.? 290 | S<NKEB??RB???B??33?? 291 | n @ B?u? B??? B?G?? 292 | S6LHB??qB??' 293 | ?? @ B?L? B?? B??f? 294 | S<RHTB^ffBlBi? ?? 295 | ?? @ Bj?? Bhz? Bi?? 296 | S8COLBA???A???? 297 | ?d S A?33 A??? A?G?? 298 | S<HEPA???B??B?H?g 299 | ?{ S B?{ Bff Bff? 300 | S=CSIQA??\A?A??\+? 301 | ? ? A?? A??? A?33? 302 | S<GASB=??Bc??BG???V 303 | ?s @ BG?H BDQ? BDG?? 304 | S=ULTAB?B?B?? 305 | ? 306 | ?? @ B??= B?ff B?8R? 307 | S7SLBB???B?G? 308 | y @ B?=q B?33 B?#?? 309 | S=FANGB?B?B?(?? 310 | t ? B?}q B??q B? 311 | =? 312 | S8NFLXC?}qC??3;?? 313 | 2 @ C??? C?Ff C?&f? 314 | S=FSLRB???B?8RB?33]?? 315 |  @ B??H B?p? B?5?? 316 | S=REGNC?ffC???C?]q ?? 317 | t @ C?}? C?? C?XR? 318 | S=LIOX@?ff@???@?Q??? 319 | ?? ?6 @? @??R @???? 320 | S<USBB"?RB$? 321 | B"?RU? 322 | ?? @ B% B"Q? B$Q?? 323 | S8TRLABiffBj? e7 324 | ?? @ Bnz? Bh 325 | = Bg??? 326 | S=CREEB@??BC?BC ? 327 |  @ BD? 328 | BAff BB??? 329 | S=UNIS???\@ @? 330 | ?? 331 | ?? S @? 332 | @G? @=q? 333 | S=KNDIA?33A?(?A???+ ? 334 | ? B A? A??\ A?Q?? 335 | S<UALB*B-??B- 336 | =Yq 337 | ? @ B2?H B*? 338 |  B/?\? 339 | S<DFSBlBm?Bl??+?? 340 | ?s  Bpa Bl BoG?? 341 | S<JKSA???A??\A?ff? 342 | ? { A??? A??R A?\)? 343 | S<ALLBa??Bo??Bm=q/?? 344 | ?? @ Bo?H Bl? BnQ?? 345 | S<HUMB??HB??RB????? 346 | ?? @ B??H B? B?#?? 347 | S<ALKB(B-G?B+ 348 | =?L 349 | ?? @ B-\) B)\) B+??? 350 | S<CSXA??RA???A?(?X?? 351 | & @ A? 352 | = A?? A?z?? 353 | S=QIHUB?Q?B?ffB????E 354 | A @ B??e B?33 B???? 355 | S=DSCI@?Q?As 356 | =AQ??_ 357 | ?? S A?H Aff A?? 358 | S<ALBBf?BxffBrff?? 359 | ?? S Bw? Bqp? Bu?? 360 | S<HCPB 361 | =B%ffB"? 362 | ?? 363 | ?? @ B#?\ B"=q B"=q? 364 | S<HCNBt??B?u?B|\)?@ 365 | ?? ?? B~G? B|? B}33? 366 | S<HTZA?33A?33A?\)PJ 367 | ?? @ A? A?=q A???? 368 | S=FTNTA?z?A?Q?A?? 369 | ?U R A?z? A?z? A?{? 370 | S=SODABB??B 371 | = j 372 | ? @ B B B? 373 | S<AKSA??A??A?d?? 374 | ?? @ A?R A AQ?? 375 | S<CATB?B??B??HW? 376 |  @ B??? B??? B???? 377 | S=NMBLA?A??HA??? 378 | ?? ? A??? A?33 A??? 379 | S7YOD@+?@(???5 380 | ? @ @*=q @' 381 | = @' 382 | =? 383 | S<CARBl??Bt?Bq 384 | =?? 385 | ?? @ Brff Bl?R Bmp?? 386 | S<GPSB'ffB){B ??2U\ 387 |  b @ B#? B \) B!?H? 388 | S<POTB?HB ??B ?BU? 389 |  ? @ B B ? B 33? 390 | S7GPNB??\B? 391 | =?? 392 | ?? S B?z? B?? B???? 393 | S;ZUBG?B?BIg 394 | ?? @ B? BQ? Bz?? 395 | S=YRCWA?A?33A?A? 396 | ? ? A? A?33 A?ff? 397 | S8SFUNA:=qA8???? 398 | ? @ A<(? A? A%p?? 399 | S<OKSB^BhffB^???{ 400 | ? ?3 B_ff BVp? B]Q?? 401 | S<CRMBRBX??BW*? 402 | ?? @ BZ?? BUp? BW?\? 403 | S=GOGOAvffA{33A{33Im 404 |  ? @ A??? Avff A{ 405 | =? 406 | S<HBIB??B???B??\?? 407 | ?? @ B?z? B? B???? 408 | S=TRIPB?B?B??fu? 409 | ?? @ B?G? B?? B?\? 410 | S<KMPB???B?B???4? 411 | d @ B?33 B?33 B??)? 412 | S<REE??z???? 413 | ??Q?? 414 | ?u @ ?? ??z? ?? 415 | =? 416 | S=EVHCA?B B???? 417 | ?? @ B B 33 B ??? 418 | S<WFMB33B?\BG?~g9 419 | : @ B=q B?? B??? 420 | S;YYB??)B?Q?B?D?? 421 | ? @ B? B??? B??? 422 | S7AJGB,?\B3???? 423 | ?? R B4?? B333 B3Q?? 424 | S=JAZZC??C C?? 425 | C @ C? C C?? 426 | S=RJETAAPA?\d? 427 | ?? ?? A"?H A? 428 |  A (?? 429 | S=EBAYBUG?BV??BUp??X? 430 | [ @ BY BU? BU? 431 | ? 432 | S;HDB?B?L?B???=?b 433 | ?? @ B?#? B??{ B? 434 | =? 435 | S<WFCBF??BG(?BF???I? 436 | T ? BI?? BFff BH=q? 437 | S<MCDB?k?B???B???1?f 438 | ? @ B? B?y> B???? 439 | S=HART@??A33@?ff?? 440 | ? @ @?Q? @?? 441 |  @?z?? 442 | S;HAAQ??A`??AXz? 4 443 | ?? @ A_ 444 | = AU?? AYp?? 445 | S=DATABq33BwffBu=q  446 |   BxG? Bq?? Bu\)? 447 | S=QSIIA`A?Ax???? 448 | ?c @ A{? 449 | Av=q Aw\)? 450 | S=YNDXA???A?A??1W 451 | ?? @ A??? A?=q A? 452 | =? 453 | S;GYA??HA??HA?=q 454 | ?? 455 | ?? ?2 A??\ A??? A???? 456 | S=JBLUA.=qA4A2ff? 457 | ? @ A4Q? A.?R A.?R? 458 | S=SINAB;33B?? B;Q? B=?\? 460 | S=AAPLB?aHB?ffB?ff??? 461 | 3 @ B??f B?33 B???? 462 | S;GSC(? 463 | C,#?C)?l? 464 | ? @ C+? C(?q C)?q? 465 | S=IDXXB?\)C?fB??=? 466 | ?U M B??H B??H B?k?? 467 | S=FEYEA???A???A???G?? 468 | 3 ? B\) A??? A?;d? 469 | S<WENAQ?A33A?\?@E 470 | ?U @ A? @?ff @?\)? 471 | S=TSLAC|?C|??C|?r? 472 | 6 6 C?X Cy? Cx?? 473 | S;GMBQ?B(?Bp??f? 474 | ' @ B? B33 B??? 475 | S=ERII@s33@?z?@xQ??? 476 | ?? @ @? @f?  @?(?? 477 | S<AIGBO??BP?BP=q?c? 478 | ? @ BT BO?? BR? 479 | S=AMZNC??RC???C???,?Q 480 | ? @ C??O C??? C???? 481 | S=SIMOA???A?A?ffB? 482 | ? @ A?z? A?{ A?G?? 483 | S<HRBA?=qB?HB?? 484 | ?s @ B?\ B?? B33? 485 | S=AKAMB`{Bc??Bb333? 486 | ?? @ BeUM Bb 487 | = Bb(?? 488 | S=PANWB?{B???B??? 489 | (u 490 | ?? @ B?33 B?? B???? 491 | S<EXCA?=qA? 492 | =A?z?n?a 493 | ?s @ A?ff A?? A?p?? 494 | S=PLUG@?? 495 | @?z?@?? 496 | ?b? 497 | / @ @??\ @??\ @?=q? 498 | S<TJXBTQ?B\BU??!? 499 | ?? @ BYp? BU?? BW??? 500 | S7GMTBj??Bw 501 | =E 502 | ?? S By? Bu?$ Bv?? 503 | S:ZC 504 | C ?C 505 | :??? 506 |  @ C5? C 507 |  C 508 | L?? 509 | S=KATEBB? 510 | B? & 511 | ?? ?? B\) B{ B=q? 512 | S:XB 513 | B ffB {X? 514 | ?s @ B ?? B ?? B ?? 515 | S:VCPCQ?CQ 516 | =$?? 517 | ? @ CSs? CP?? CR??? 518 | S;FBB?ffB?u?B?p?E?? 519 | 9 @ B? B?ff B???? 520 | S:NB??B???B???l? 521 | ?? @ B?u? B?? B?(?? 522 | S=MRVLAHAU??APB?? 523 | ?? @ ASG? AL?? AQp?? 524 | S<GMEB"=qB*=qB#z??M 525 | Z @ B'?? B#ff B&? 526 | S<MRKB^?B`B^?\n? 527 | P @ Bb=q B^G? B_?R? 528 | S:KBwB}?By??* 529 | ? @ B|p? Bw?H B{?? 530 | S:I??A??A?(??? 531 | ?? S A??! A?p? A?\)? 532 | S<COHB 533 | =qB ??B G?,t 534 | } @ B v? B 535 | ?H B ff? 536 | S=MNGA?????????{?J 537 | ? ?t ???? ??? ???? 538 | S<COFB?ffB?p?B?{#?{ 539 | ?s @ B??{ B??? B???? 540 | S8MRVCAN{Aa??? 541 | ? @ Ac? 542 | Aa? Ad? 543 | S=ARIA@??H@???@???5?G 544 | : @ @?ff @??? @?(?? 545 | S<DALB ?B ??Bp??? 546 | = @ B?) B ?H B{? 547 | S6VZBABB???q? 548 | ? @ BEG? BA?? BD?? 549 | S=WBAIBB AD?R AAp? AC33? 589 | S=SWKSBKBS??BK{.YH 590 | j @ BS BK? BP??? 591 | S8RATEA{33A??? ? 592 | ?? @ A??! A?z? A?Q?? 593 | S<HONB?#?B???B???6?? 594 | < @ B??? B?. B??? 595 | S<HOGBp{B?Br??? 596 | ?? @ Bx? 597 | Br 598 | = Bv??? 599 | S7THIBsffBs???* 600 | ?? @ Bs?k Bm?\ Blz?? 601 | S;DEB?p?B???B??q'?y 602 | ? @ B?33 B?z? B???? 603 | S=KORSB?\B???B??&F 604 | R @ B?LJ B??? B?k?? 605 | S;DDB(?B???B???4?? 606 | ?? @ B?G? B?p? B??=? 607 | S=INFYBb??BjBd??%?? 608 | ?? @ Bf Bc?? Ba{? 609 | S8WUBABE33BC?R?? 610 | ? @ BK?? BBz? BE? 611 | ? 612 | S<WSMB?.B??B???h? 613 | ?? @ B??? B?W 614 |  B???? 615 | S=CERNBXffB`B\!?| 616 | ?? @ BaL? BZ? B_??? 617 | S=CNQRB?ffB?B??qB? 618 | ?U S B?z? B??? B?L?? 619 | S=BMRNB\B?B~??? 620 | ?? @ B??? B| B?8R? 621 | S<CMIC ?C ?C ^?)d 622 | ? @ C ?H C  C ^?? 623 | S<CMGD&@D(?\D'??l* 624 | ? @ D)?? D'J= D(@? 625 | S<AETB?33B???B?p?>N? 626 | ? @ B?8R B??? B???? 627 | S<LLLB?B?B?.g? 628 | ?? @ B?#? B?? B???? 629 | S<IRMBB?HB? 630 | QS 631 | ?? S B Bff B\)? 632 | S;UAB?B?B?z?7 633 | ? @ B??? B??? B?z?? 634 | S=APPY???R??Q???z?? 635 | ? @ ??Q? ???? ????? 636 | S7TGTB`BfKb 637 | c @ Bip? Be{ Bg?H? 638 | S7ETPB]??Bap?1? 639 | ? @ Bc?H B^ 640 | = B^ 641 | =? 642 | HTG?o?? -------------------------------------------------------------------------------- /vendor/tmp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | OK 4 | 5 | 6 | 861214584 7 | 8 | Top 10.tos 9 | 292979390 10 | 11 | 12 | 0 13 | 14 | FB 15 | FB 16 | FACEBOOK INC CL A 17 | E 18 | 19 | LONG 20 | 0 21 | 0 22 | 23 | 0XIVXIVCREDIT SUISSE NASSAU BRH INVRS VIX STERMELONG000SINASINASINA CORP ORDELONG000TWTRTWTRTWITTER INC COMELONG000VIXVIXELONG000YELPYELPYELP INC CL AELONG000SUNESUNESUNEDISON INC COMELONG000KNDIKNDIKANDI TECHNOLOGIES GROUP INC COMELONG000DISHDISHDISH NETWORK CORP CL AELONG000WDAYWDAYWORKDAY INC CL AELONG000SBUXSBUXSTARBUCKS CORP COMELONG000TSLATSLATESLA MTRS INC COMELONG000$TICK$TICKNYSE Composite TICKILONG000GPROGPROGOPRO INC CL AELONG000GTATGTATGT ADVANCED TECHNOLOGIES INC COMELONG000VXXVXXBARCLAYS BK PLC IPATH S&P500 VIXELONG000AALAALAMERICAN AIRLS GROUP INC COMELONG000DALDALDELTA AIR LINES INC DEL COM NEWELONG000RFMDRFMDRF MICRODEVICES INC COMELONG000UAUAUNDER ARMOUR INC CL AELONG000UALUALUNITED CONTL HLDGS INC COMELONG000ONVOONVOORGANOVO HLDGS INC COMELONG000ZZZILLOW INC CL AELONG000TRLATRLATRULIA INC COMELONG000GILDGILDGILEAD SCIENCES INC COMELONG000QLIKQLIKQLIK TECHNOLOGIES INC COMELONG000PPPANDORA MEDIA INC COMELONG000AMZNAMZNAMAZON COM INC COMELONG000LOGMLOGMLOGMEIN INC COMELONG000LPNTLPNTLIFEPOINT HOSPITALS INC COMELONG000QIHUQIHUQIHOO 360 TECHNOLOGY CO LTD ADSELONG000VRSVRSVERSO PAPER CORP COMELONG000OSTKOSTKOVERSTOCK COM INC DEL COMELONG00 24 | 25 | hot trades.tos2603799010VXXVXXBARCLAYS BK PLC IPATH S&P500 VIXELONG000VIXVIXELONG000SPXSPXSpectrum Rare Earths LtdELONG000TWTRTWTRTWITTER INC COMELONG000YELPYELPYELP INC CL AELONG000YYYYYY INC ADS REPCOM CLAELONG000SCTYSCTYSOLARCITY CORP COMELONG000NPSPNPSPNPS PHARMACEUTICALS INC COMELONG000SINASINASINA CORP ORDELONG000LNKDLNKDLINKEDIN CORP COM CL AELONG000QIHUQIHUQIHOO 360 TECHNOLOGY CO LTD ADSELONG000INVNINVNINVENSENSE INC COMELONG000JCPJCPPENNEY J C INC COMELONG000SFUNSFUNSOUFUN HLDGS LTD ADRELONG000KNDIKNDIKANDI TECHNOLOGIES GROUP INC COMELONG000ZUZUZULILY INC CL AELONG000TASRTASRTASER INTL INC COMELONG000GTATGTATGT ADVANCED TECHNOLOGIES INC COMELONG000ZZZILLOW INC CL AELONG000HZNPHZNPHORIZON PHARMA INC COMELONG000HPQHPQHEWLETT PACKARD CO COMELONG000CSIQCSIQCANADIAN SOLAR INC COMELONG000PPPANDORA MEDIA INC COMELONG000ADBEADBEADOBE SYS INC COMELONG000PEIXPEIXPACIFIC ETHANOL INC COM PAR $.001ELONG000NUSNUSNU SKIN ENTERPRISES INC CL AELONG000CAMTCAMTCAMTEK LTD ORDELONG000PANWPANWPALO ALTO NETWORKS INC COMELONG000NMBLNMBLNIMBLE STORAGE INC COMELONG000CREGCREGCHINA RECYCLING ENERGY CORP COMELONG000CARACARACARA THERAPEUTICS INC COMELONG000HPJHPJHIGHPOWER INTL INC COMELONG000BIDUBIDUBAIDU INC SPON ADR REP AELONG000XXIIXXII22ND CENTY GROUP INC COMELONG000ARTXARTXAROTECH CORP COM NEWELONG000COVCOVCOVIDIEN PLC SHSELONG000QTWWQTWWQUANTUM FUEL SYS TECH WORLDW COM PAR $0.02ELONG000ATATATLANTIC PWR CORP COM NEWELONG000HIMXHIMXHIMAX TECHNOLOGIES INC SPONSORED ADRELONG000FBFBFACEBOOK INC CL AELONG000ECYTECYTENDOCYTE INC COMELONG000PAYXPAYXPAYCHEX INC COMELONG000WWEWWEWORLD WRESTLING ENTMT INC CL AELONG000SPCBSPCBSUPERCOM LTD NEW SHS NEWELONG000EXTREXTREXTREME NETWORKS INC COMELONG000IBMIBMINTERNATIONAL BUSINESS MACHS COMELONG000CLDXCLDXCELLDEX THERAPEUTICS INC NEW COMELONG000WDAYWDAYWORKDAY INC CL AELONG000RHTRHTRED HAT INC COMELONG000RMAXRMAXRE MAX HLDGS INC CL AELONG00 --------------------------------------------------------------------------------