All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.studerw.tda.client.HttpTdaClient Maven / Gradle / Ivy

There is a newer version: 2.4.3.1
Show newest version
package com.studerw.tda.client;

import com.studerw.tda.http.LoggingInterceptor;
import com.studerw.tda.http.cookie.CookieJarImpl;
import com.studerw.tda.http.cookie.store.MemoryCookieStore;
import com.studerw.tda.model.account.Order;
import com.studerw.tda.model.account.OrderRequest;
import com.studerw.tda.model.account.OrderRequestValidator;
import com.studerw.tda.model.account.SecuritiesAccount;
import com.studerw.tda.model.auth.AuthToken;
import com.studerw.tda.model.history.PriceHistReq;
import com.studerw.tda.model.history.PriceHistReqValidator;
import com.studerw.tda.model.history.PriceHistory;
import com.studerw.tda.model.instrument.FullInstrument;
import com.studerw.tda.model.instrument.Instrument;
import com.studerw.tda.model.instrument.Query;
import com.studerw.tda.model.marketdata.Mover;
import com.studerw.tda.model.marketdata.MoversReq;
import com.studerw.tda.model.markethours.Hours;
import com.studerw.tda.model.option.OptionChain;
import com.studerw.tda.model.option.OptionChainReq;
import com.studerw.tda.model.quote.Quote;
import com.studerw.tda.model.transaction.Transaction;
import com.studerw.tda.model.transaction.TransactionRequest;
import com.studerw.tda.model.transaction.TransactionRequestValidator;
import com.studerw.tda.model.user.Preferences;
import com.studerw.tda.model.user.StreamerSubscriptionKeys;
import com.studerw.tda.model.user.UserPrincipals;
import com.studerw.tda.model.user.UserPrincipals.Field;
import com.studerw.tda.parse.DefaultMapper;
import com.studerw.tda.parse.TdaJsonParser;
import com.studerw.tda.parse.Utils;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;

import okhttp3.*;
import okhttp3.HttpUrl.Builder;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.studerw.tda.client.TdaClientProperty.*;

/**
 * HTTP implementation of {@link TdaClient} which uses OKHttp3 under the hood and uses the new OAuth
 * based security.
 * This is a thread safe class.
 *
 * @see OKHttp3 from Square
 */
public class HttpTdaClient implements TdaClient {

  protected static final int LOGGING_BYTES = -1;
  protected static final DateTimeFormatter ISO_FORMATTER = DateTimeFormatter.BASIC_ISO_DATE;
  protected static final String DEFAULT_PATH = "https://api.tdameritrade.com/v1";
  private static final Logger LOGGER = LoggerFactory.getLogger(HttpTdaClient.class);
  private static final String AUTHORIZATION_HEADER = "Authorization";
  private static final String LOCATION_HEADER = "location";

  final TdaJsonParser tdaJsonParser = new TdaJsonParser();
  final OkHttpClient httpClient;

  Properties tdaProps;
  private HttpUrl httpUrl;

  /**
   * Using this constructor will assume there are properties found at {@code
   * classpath:/tda-api.properties}. This props file can include:
   * 
    *
  • tda.token.refresh
  • *
  • tda.token.access (optional, it will be automatically retrieved using refresh token if not specified or expired)
  • *
  • tda.client_id (or sometimes referenced as Consumer Key and it should not have @AMER.OAUTHAP appended
  • *
  • tda.url=https://api.tdameritrade.com/v1
  • *
  • tda.debug.bytes.length=-1 (How many bytes of logging interceptor debug to print, -1 is unlimited)
  • *
* *

There are no defaults for the tda.token.refresh and tda.client_id (your consumer key). * If they are not set, an exception will be thrown * Note that the client id should not have appended the @AMER.OAUTHAP part * that is used when refreshing your OAuth token. *

*/ public HttpTdaClient() { this(null, null); } /** *

* To avoid using a properties file, you can define anything that would be in {@code * tda-api.properties} file. This includes: *

* *
    *
  • tda.token.refresh
  • *
  • tda.client_id
  • *
  • tda.url=https://api.tdameritrade.com/v1
  • *
  • tda.debug.bytes.length=-1 (How many bytes of logging interceptor debug to print, -1 is unlimited)
  • *
* *

There are no defaults for tda.token.refresh and tda.client_id (consumer key). If they * are not set, an exception will be thrown. Note that sometimes TDA uses Consumer Key * instead of the term client id. They are the same. * The client id should not have appended the @AMER.OAUTHAP part that is used when refreshing your OAuth token

* * @param props required properties */ public HttpTdaClient(Properties props) { this(null, props); } /** * Using this constructor allows to supply own instance of http client. * It's useful for connecting to multiple TDA accounts and re-using single shared http client. * * @param httpClient http client to use * @param props required properties */ public HttpTdaClient(OkHttpClient httpClient, Properties props) { LOGGER.info("Initiating HttpTdaClient..."); this.tdaProps = (props == null) ? initTdaProps() : props; validateProps(this.tdaProps); this.httpClient = (httpClient == null) ? initHttpClient() : httpClient; } protected static Properties initTdaProps() { try (InputStream in = HttpTdaClient.class.getClassLoader() .getResourceAsStream("tda-api.properties")) { Properties tdProperties = new Properties(); tdProperties.load(in); return tdProperties; } catch (IOException e) { throw new IllegalArgumentException( "Could not load default properties from com.studerw.tda.tda-api.properties in classpath"); } } private OkHttpClient initHttpClient() { return new OkHttpClient.Builder(). cookieJar(new CookieJarImpl(new MemoryCookieStore())). addInterceptor(new LoggingInterceptor("TDA_HTTP", Integer.parseInt(tdaProps.getProperty(DEBUG_BYTES_LENGTH)))). build(); } /** * validates the necessary props like access token, refresh token and client id (consumer key). If others are missing, just use * friendly defaults. * * @param tdaProps the required props to validate */ protected static void validateProps(Properties tdaProps) { LOGGER.trace("Validating props: {}", tdaProps.toString()); String clientId = tdaProps.getProperty(CLIENT_ID); if (StringUtils.isBlank(clientId)) { throw new IllegalArgumentException( "Missing tda.client_id property. This is obtained from TDA developer API when registering an app"); } String refreshToken = tdaProps.getProperty(REFRESH_TOKEN); if (StringUtils.isBlank(refreshToken)) { throw new IllegalArgumentException( "Missing tda.token.refresh property. This is obtained from the TDA developer API page when creating a temporary authentication token"); } String accessToken = tdaProps.getProperty(ACCESS_TOKEN); if (StringUtils.isBlank(accessToken)) { // This gets updated using the refresh code - the first call will always fail, forcing a // new access_token to be set. tdaProps.setProperty(ACCESS_TOKEN, "UNSET"); } String url = tdaProps.getProperty(TDA_URL); if (StringUtils.isBlank(url)) { tdaProps.setProperty(TDA_URL, DEFAULT_PATH); } if (tdaProps.get(DEBUG_BYTES_LENGTH) == null) { tdaProps.setProperty(DEBUG_BYTES_LENGTH, "-1"); } } @Override public PriceHistory priceHistory(String symbol) { symbol = StringUtils.upperCase(symbol); LOGGER.info("price history for symbol: {}", symbol); if (StringUtils.isBlank(symbol)) { throw new IllegalArgumentException("symbol cannot be empty"); } HttpUrl url = baseUrl("marketdata", symbol, "pricehistory").build(); Request request = new Request.Builder().url(url).headers(defaultHeaders()).build(); try (Response response = this.call(request)) { checkResponse(response, false); return tdaJsonParser.parsePriceHistory(response.body().byteStream()); } } @Override public PriceHistory priceHistory(PriceHistReq priceHistReq) { LOGGER.info("PriceHistory: {}", priceHistReq); List violations = PriceHistReqValidator.validate(priceHistReq); if (violations.size() > 0) { throw new IllegalArgumentException(violations.toString()); } Builder urlBuilder = baseUrl("marketdata", priceHistReq.getSymbol(), "pricehistory"); if (priceHistReq.getStartDate() != null) { urlBuilder.addQueryParameter("startDate", String.valueOf(priceHistReq.getStartDate())); } if (priceHistReq.getEndDate() != null) { urlBuilder.addQueryParameter("endDate", String.valueOf(priceHistReq.getEndDate())); } if (priceHistReq.getFrequency() != null) { urlBuilder.addQueryParameter("frequency", String.valueOf(priceHistReq.getFrequency())); } if (priceHistReq.getFrequencyType() != null) { urlBuilder.addQueryParameter("frequencyType", priceHistReq.getFrequencyType().name()); } if (priceHistReq.getPeriod() != null) { urlBuilder.addQueryParameter("period", String.valueOf(priceHistReq.getPeriod())); } if (priceHistReq.getPeriodType() != null) { urlBuilder.addQueryParameter("periodType", priceHistReq.getPeriodType().name()); } if (priceHistReq.getExtendedHours() != null) { urlBuilder.addQueryParameter("needExtendedHoursData", String.valueOf(priceHistReq.getExtendedHours())); } Request request = new Request.Builder().url(urlBuilder.build()). headers(defaultHeaders()) .build(); try (Response response = this.call(request)) { checkResponse(response, false); return tdaJsonParser.parsePriceHistory(response.body().byteStream()); } } @Override public List fetchQuotes(List symbols) { LOGGER.info("Fetching quotes: {}", symbols); HttpUrl url = baseUrl("marketdata", "quotes") .addQueryParameter("symbol", String.join(",", symbols)) .build(); Request request = new Request.Builder().url(url) .headers(defaultHeaders()) .build(); try (Response response = this.call(request)) { checkResponse(response, false); return tdaJsonParser.parseQuotes(response.body().byteStream()); } } @Override public Quote fetchQuote(String symbol) { List quotes = fetchQuotes(Collections.singletonList(symbol)); return quotes.get(0); } @Override public SecuritiesAccount getAccount(String accountId, boolean positions, boolean orders) { LOGGER.info("GetAccount[id={}], positions={}, orders={}", accountId, positions, orders); if (StringUtils.isBlank(accountId)) { throw new IllegalArgumentException("accountId cannot be blank."); } List args = new ArrayList<>(); if (positions) { args.add("positions"); } if (orders) { args.add("orders"); } final Builder accountsBldr = baseUrl("accounts", accountId); if (!Utils.isNullOrEmpty(args)) { accountsBldr.addQueryParameter("fields", String.join(",", args)); } final URL url = accountsBldr.build().url(); final Request request = new Request.Builder().url(url) .headers(defaultHeaders()) .build(); try (Response response = this.call(request)) { checkResponse(response, false); return tdaJsonParser.parseAccount(response.body().byteStream()); } } @Override public List getAccounts(boolean positions, boolean orders) { LOGGER.info("GetAccount positions={}, orders={}", positions, orders); List args = new ArrayList<>(); if (positions) { args.add("positions"); } if (orders) { args.add("orders"); } final Builder accountsBldr = baseUrl("accounts"); if (!Utils.isNullOrEmpty(args)) { accountsBldr.addQueryParameter("fields", String.join(",", args)); } final URL url = accountsBldr.build().url(); final Request request = new Request.Builder().url(url) .headers(defaultHeaders()) .build(); try (Response response = this.call(request)) { checkResponse(response, false); return tdaJsonParser.parseAccounts(response.body().byteStream()); } } @Override public List getMarketHours(List marketTypes) { return getMarketHours(marketTypes, null); } @Override public List getMarketHours(List marketTypes, LocalDateTime date) { if(marketTypes == null || (marketTypes != null && marketTypes.size() == 0)) { throw new IllegalArgumentException("One or more Hours.MarketType(s) are required"); } if(date != null && date.isBefore(LocalDateTime.now())) { throw new IllegalArgumentException("Date must be a future date."); } String stringMarketTypes = ""; for(Hours.MarketType mt: marketTypes) { if(stringMarketTypes.length() > 0) { stringMarketTypes += ","; } stringMarketTypes += mt.toString(); } LOGGER.info("GetMarketHours[marketType={}]", stringMarketTypes); List args = new ArrayList<>(); final Builder hoursBldr = baseUrl("marketdata", "hours"); if(stringMarketTypes.length() > 0) { hoursBldr.addQueryParameter("markets", stringMarketTypes); } else { throw new IllegalArgumentException("One or more Hours.MarketType(s) are required"); } if(date != null) { hoursBldr.addQueryParameter("date", date.format(DateTimeFormatter.ISO_DATE_TIME)); } final URL url = hoursBldr.build().url(); final Request request = new Request.Builder().url(url) .headers(defaultHeaders()) .build(); try (Response response = this.call(request)) { checkResponse(response, false); return tdaJsonParser.parseMarketHours(response.body().byteStream()); } catch (IOException e) { throw new TdaClientException(e); } } @Override public Long placeOrder(String accountId, Order order) { LOGGER.info("Placing Order for account[{}] -> {}", accountId, order); if (StringUtils.isBlank(accountId)) { throw new IllegalArgumentException("accountId cannot be blank."); } HttpUrl url = baseUrl("accounts", accountId, "orders") .build(); String json = DefaultMapper.toJson(order); RequestBody body = RequestBody.create(MediaType.parse("application/json"), json); Request request = new Request.Builder().url(url). headers(defaultHeaders()) .post(body) .build(); try (Response response = this.call(request)) { checkResponse(response, false); if (response.code() != 201) { LOGGER.warn("Expected 201 response, but received " + response.code()); } String location = response.header(LOCATION_HEADER); if (null == location) { throw new TdaClientException("No location header found. Can't get order id."); } return Long.valueOf(location.substring(location.lastIndexOf("/") + 1)); } } @Override public List fetchOrders(String accountId, OrderRequest orderRequest) { LOGGER.info("FetchOrders for account[{}] with request: {}", accountId, orderRequest); if (StringUtils.isBlank(accountId)) { throw new IllegalArgumentException("accountId cannot be blank."); } List violations = OrderRequestValidator.validate(orderRequest); if (violations.size() > 0) { throw new IllegalArgumentException(violations.toString()); } Builder urlBuilder = baseUrl("accounts", accountId, "orders"); if (orderRequest.getMaxResults() != null) { urlBuilder.addQueryParameter("maxResults", String.valueOf(orderRequest.getMaxResults())); } if (orderRequest.getToEnteredTime() != null) { urlBuilder .addQueryParameter("toEnteredTime", Utils.toTdaISO8601(orderRequest.getToEnteredTime())); } if (orderRequest.getFromEnteredTime() != null) { urlBuilder.addQueryParameter("fromEnteredTime", Utils.toTdaISO8601(orderRequest.getFromEnteredTime())); } if (orderRequest.getStatus() != null) { urlBuilder.addQueryParameter("status", orderRequest.getStatus().name()); } Request request = new Request.Builder().url(urlBuilder.build()) .headers(defaultHeaders()) .build(); try (Response response = this.call(request)) { checkResponse(response, false); return tdaJsonParser.parseOrders(response.body().byteStream()); } } @Override public List fetchOrders(OrderRequest orderRequest) { LOGGER.info("FetchOrders all orders with request: {}", orderRequest); List violations = OrderRequestValidator.validate(orderRequest); if (violations.size() > 0) { throw new IllegalArgumentException(violations.toString()); } Builder urlBuilder = baseUrl("orders"); if (orderRequest.getMaxResults() != null) { urlBuilder.addQueryParameter("maxResults", String.valueOf(orderRequest.getMaxResults())); } if (orderRequest.getToEnteredTime() != null) { urlBuilder .addQueryParameter("toEnteredTime", Utils.toTdaISO8601(orderRequest.getToEnteredTime())); } if (orderRequest.getFromEnteredTime() != null) { urlBuilder.addQueryParameter("fromEnteredTime", Utils.toTdaISO8601(orderRequest.getFromEnteredTime())); } if (orderRequest.getStatus() != null) { urlBuilder.addQueryParameter("status", orderRequest.getStatus().name()); } Request request = new Request.Builder().url(urlBuilder.build()) .headers(defaultHeaders()) .build(); try (Response response = this.call(request)) { checkResponse(response, false); return tdaJsonParser.parseOrders(response.body().byteStream()); } } @Override public List fetchOrders() { LOGGER.info("FetchOrders all orders."); Builder urlBuilder = baseUrl("orders"); Request request = new Request.Builder().url(urlBuilder.build()) .headers(defaultHeaders()) .build(); try (Response response = this.call(request)) { checkResponse(response, false); return tdaJsonParser.parseOrders(response.body().byteStream()); } } @Override public Order fetchOrder(String accountId, Long orderId) { LOGGER.info("Fetching for account[{}] order[{}]", accountId, orderId); if (StringUtils.isBlank(accountId)) { throw new IllegalArgumentException("accountId cannot be blank."); } if (orderId == null) { throw new IllegalArgumentException("orderId cannot be blank."); } Builder urlBuilder = baseUrl("accounts", accountId, "orders", String.valueOf(orderId)); Request request = new Request.Builder().url(urlBuilder.build()).headers(defaultHeaders()) .build(); try (Response response = this.call(request)) { checkResponse(response, false); return tdaJsonParser.parseOrder(response.body().byteStream()); } } @Override public void cancelOrder(String accountId, String orderId) { LOGGER.info("Cancelling order: {} for account[{}].", orderId, accountId); if (StringUtils.isBlank(accountId)) { throw new IllegalArgumentException("accountId cannot be blank."); } if (StringUtils.isBlank(orderId)) { throw new IllegalArgumentException("orderId cannot be blank."); } HttpUrl url = baseUrl("accounts", accountId, "orders", orderId).build(); Request request = new Request.Builder().url(url). headers(defaultHeaders()) .delete() .build(); try (Response response = this.call(request)) { checkResponse(response, false); } } @Override public Instrument getBond(String cusip) { return this.getInstrumentByCUSIP(cusip); } @Override public List queryInstruments(Query query) { LOGGER.info("Querying for Instruments with query: {}", query); if (query == null || StringUtils.isEmpty(query.getSearchStr()) || query.getQueryType() == null) { throw new IllegalArgumentException( "The instrument query must have both a searchStr and QueryType set."); } HttpUrl url = baseUrl("instruments") .addQueryParameter("symbol", query.getSearchStr()) .addQueryParameter("projection", query.getQueryType().getQueryType()) .build(); Request request = new Request.Builder().url(url).headers(defaultHeaders()).build(); try (Response response = this.call(request)) { checkResponse(response, false); return tdaJsonParser.parseInstrumentMap(response.body().byteStream()); } } @Override public FullInstrument getFundamentalData(String id) { LOGGER.info("Fetching Fundamental Instrument data with id: {}", id); if (StringUtils.isBlank(id)) { throw new IllegalArgumentException("Id cannot be blank."); } HttpUrl url = baseUrl("instruments") .addQueryParameter("symbol", id) .addQueryParameter("projection", "fundamental") .build(); Request request = new Request.Builder().url(url).headers(defaultHeaders()).build(); try (Response response = this.call(request)) { checkResponse(response, false); final List fullInstruments = tdaJsonParser .parseFullInstrumentMap(response.body().byteStream()); if (fullInstruments.size() != 1) { throw new TdaClientException( "Expecting a single instrument but received: " + fullInstruments.size()); } return fullInstruments.get(0); } } @Override public List fetchMovers(MoversReq moversReq) { LOGGER.info("Fetching Movers with req: {}", moversReq); if (moversReq.getIndex() == null) { throw new IllegalArgumentException("The index cannot be empty."); } Builder urlBuilder = baseUrl("marketdata", moversReq.getIndex().getIndex(), "movers"); if (moversReq.getChange() != null) { urlBuilder.addQueryParameter("change", moversReq.getChange().getChange()); } if (moversReq.getDirection() != null) { urlBuilder.addQueryParameter("direction", moversReq.getDirection().name()); } Request request = new Request.Builder().url(urlBuilder.build()).headers(defaultHeaders()) .build(); try (Response response = this.call(request)) { checkResponse(response, true); return tdaJsonParser.parseMovers(response.body().byteStream()); } } @Override public OptionChain getOptionChain(OptionChainReq chainRequest) { LOGGER.info("get option chain for: {}", chainRequest.toString()); if (StringUtils.isBlank(chainRequest.getSymbol())) { throw new IllegalArgumentException("Symbol cannot be blank."); } Builder urlBuilder = baseUrl("marketdata", "chains") .addQueryParameter("symbol", chainRequest.getSymbol().toUpperCase()) .addQueryParameter("contractType", chainRequest.getContractType().toString()) .addQueryParameter("strategy", chainRequest.getStrategy().toString()) .addQueryParameter("range", chainRequest.getRange().toString()) .addQueryParameter("optionType", chainRequest.getOptionType().toString()); if(chainRequest.getStrikeCount() != null && chainRequest.getStrikeCount() > 0) { urlBuilder.addQueryParameter("strikeCount", chainRequest.getStrikeCount().toString()); } if(chainRequest.getIncludeQuotes() != null) { urlBuilder.addQueryParameter("includeQuotes", chainRequest.getIncludeQuotes().toString()); } if(chainRequest.getInterval() != null) { urlBuilder.addQueryParameter("interval", chainRequest.getInterval().toString()); } if(chainRequest.getStrike() != null) { urlBuilder.addQueryParameter("strike", chainRequest.getStrike().toString()); } if(chainRequest.getFromDate() != null) { urlBuilder.addQueryParameter("fromDate", chainRequest.getFromDate().format(DateTimeFormatter.ISO_DATE_TIME)); } if(chainRequest.getToDate() != null) { urlBuilder.addQueryParameter("toDate", chainRequest.getToDate().format(DateTimeFormatter.ISO_DATE_TIME)); } if(chainRequest.getVolatility() != null) { urlBuilder.addQueryParameter("volatility", chainRequest.getVolatility().toString()); } if(chainRequest.getUnderlyingPrice() != null) { urlBuilder.addQueryParameter("underlyingPrice", chainRequest.getUnderlyingPrice().toString()); } if(chainRequest.getInterestRate() != null) { urlBuilder.addQueryParameter("interestRate", chainRequest.getInterestRate().toString()); } if(chainRequest.getDaysToExpiration() != null) { urlBuilder.addQueryParameter("daysToExpiration", chainRequest.getDaysToExpiration().toString()); } if(chainRequest.getMonth() != null) { urlBuilder.addQueryParameter("month", chainRequest.getMonth().toString().substring(0, 3).toUpperCase()); } Request request = new Request.Builder().url(urlBuilder.build()).headers(defaultHeaders()) .build(); try (Response response = this.call(request)) { checkResponse(response, false); return tdaJsonParser.parseOptionChain(response.body().byteStream()); } } @Override public OptionChain getOptionChain(String symbol) { LOGGER.info("get option chain for symbol: {}", symbol); if (StringUtils.isBlank(symbol)) { throw new IllegalArgumentException("Symbol cannot be blank."); } OptionChainReq request = OptionChainReq.Builder.optionChainReq() .withSymbol(symbol).build(); return getOptionChain(request); } @Override public List fetchTransactions(String accountId, TransactionRequest request) { LOGGER.info("FetchTransactions for account[{}]", accountId); if (StringUtils.isBlank(accountId)) { throw new IllegalArgumentException("accountId cannot be blank."); } if (request == null) { request = new TransactionRequest(); } List violations = TransactionRequestValidator.validate(request); if (violations.size() > 0) { throw new IllegalArgumentException(violations.toString()); } Builder urlBuilder = baseUrl("accounts", accountId, "transactions"); if (StringUtils.isNotEmpty(request.getSymbol())) { urlBuilder.addQueryParameter("symbol", StringUtils.upperCase(request.getSymbol())); } if (request.getStartDate() != null) { urlBuilder.addQueryParameter("startDate", Utils.toTdaYMD(request.getStartDate())); } if (request.getEndDate() != null) { urlBuilder.addQueryParameter("endDate", Utils.toTdaYMD(request.getEndDate())); } if (request.getType() != null) { urlBuilder.addQueryParameter("type", request.getType().name()); } Request httpReq = new Request.Builder() .url(urlBuilder.build()).headers(defaultHeaders()) .build(); try (Response response = this.call(httpReq)) { checkResponse(response, true); return tdaJsonParser.parseTransactions(response.body().byteStream()); } } @Override public Transaction getTransaction(String accountId, Long transactionId) { LOGGER.info("getTransaction by id: {} for account[{}]", transactionId, accountId); if (StringUtils.isBlank(accountId)) { throw new IllegalArgumentException("accountId cannot be blank."); } if (transactionId == null) { throw new IllegalArgumentException("transaction id cannot be null."); } Builder urlBuilder = baseUrl("accounts", accountId, "transactions", String.valueOf(transactionId)); Request request = new Request.Builder().url(urlBuilder.build()).headers(defaultHeaders()) .build(); try (Response response = this.call(request)) { checkResponse(response, false); return tdaJsonParser.parseTransaction(response.body().byteStream()); } } @Override public Preferences getPreferences(String accountId) { LOGGER.info("getPreferences for account[{}]", accountId); if (StringUtils.isBlank(accountId)) { throw new IllegalArgumentException("accountId cannot be blank."); } Builder urlBuilder = baseUrl("accounts", accountId, "preferences"); Request request = new Request.Builder() .url(urlBuilder.build()) .headers(defaultHeaders()) .build(); try (Response response = this.call(request)) { checkResponse(response, false); return tdaJsonParser.parsePreferences(response.body().byteStream()); } } @Override public UserPrincipals getUserPrincipals(Field... fields) { LOGGER.info("getUserPrincipals with additional fields: {}", fields); Builder urlBuilder = baseUrl("userprincipals"); List fieldsStr = new ArrayList(); for (Field field : fields) { fieldsStr.add(field.toString()); } if (fieldsStr.size() > 0) { urlBuilder.addQueryParameter("fields", String.join(",", fieldsStr)); } Request request = new Request.Builder() .url(urlBuilder.build()) .headers(defaultHeaders()) .build(); try (Response response = this.call(request)) { checkResponse(response, false); return tdaJsonParser.parseUserPrincipals(response.body().byteStream()); } } @Override public List fetchTransactions(String accountId) { return fetchTransactions(accountId, null); } @Override public Instrument getInstrumentByCUSIP(String id) { LOGGER.info("Fetching Instrument with id: {}", id); if (StringUtils.isBlank(id)) { throw new IllegalArgumentException("Id cannot be blank."); } HttpUrl url = baseUrl("instruments", id) .addQueryParameter("fundamental", "true") .build(); Request request = new Request.Builder().url(url). headers(defaultHeaders()) .build(); try (Response response = this.call(request)) { checkResponse(response, false); return tdaJsonParser.parseInstrumentArraySingle(response.body().byteStream()); } } protected StreamerSubscriptionKeys getSubscriptionKeys(List accountsIds) { LOGGER.info("getSubscriptionKeys: {}", accountsIds); if(Utils.isNullOrEmpty(accountsIds)){ throw new IllegalArgumentException("AccountIds list must contain at least one account id"); } Builder urlBuilder = baseUrl("userprincipals", "streamersubscriptionkeys") .addQueryParameter("accountIds", String.join(",", accountsIds)); Request request = new Request.Builder() .url(urlBuilder.build()) .headers(defaultHeaders()) .build(); try (Response response = this.call(request)) { checkResponse(response, false); return tdaJsonParser.parseSubscriptionKeys(response.body().byteStream()); } } /** * @param response the tda response * @param emptyJsonOk is an empty JSON object or array actually OK (e.g. fetchMovers)? */ private void checkResponse(Response response, boolean emptyJsonOk) { if (!response.isSuccessful()) { String errorMsg = response.message(); if (StringUtils.isBlank(errorMsg)) { try { errorMsg = response.body().string(); } catch (Exception e) { LOGGER.warn("No error message nor error body"); errorMsg = "UNKNOWN"; } } String msg = String .format("Non 200 response: [%d - %s] - %s", response.code(), errorMsg, response.request().url()); throw new TdaClientException(msg); } if (!emptyJsonOk) { try { String json = response.peekBody(100).string(); if ("{}".equals(json) || "[]".equals(json)) { String msg = String .format("Empty json body: [%d - %s] - %s", response.code(), response.message(), response.request().url()); throw new TdaClientException(msg); } } catch (IOException e) { throw new TdaClientException("Error checking for JSON empty body on response"); } } } private Headers defaultHeaders() { Map defaultHeaders = new HashMap<>(); defaultHeaders.put("Accept", "application/json"); defaultHeaders.put(AUTHORIZATION_HEADER, "Bearer " + tdaProps.getProperty(ACCESS_TOKEN)); // defaultHeaders.put("Accept-Language", "en-US"); return Headers.of(defaultHeaders); } protected HttpUrl.Builder baseUrl(String... pathSegments) { if (this.httpUrl == null) { this.httpUrl = HttpUrl.parse(tdaProps.getProperty(TDA_URL)); } Builder builder = httpUrl.newBuilder(); for (String segment : pathSegments) { builder.addPathSegment(segment); } return builder; } private Response call(Request request) { Response response = null; try { response = httpClient.newCall(request).execute(); if (response.code() == 401) { String token = request.header(AUTHORIZATION_HEADER).substring(7); LOGGER.debug("Unauthorized, trying to refresh token..."); synchronized (this) { String accessToken = tdaProps.getProperty(ACCESS_TOKEN); if (token.equals(accessToken)) { LOGGER.debug("Refreshing access token..."); updateAuthToken(); } else { LOGGER.debug("Token has already been refreshed by another thread"); } return httpClient.newCall(request.newBuilder().header(AUTHORIZATION_HEADER, "Bearer " + tdaProps.getProperty(ACCESS_TOKEN)).build()).execute(); } } return response; } catch (IOException e) { throw new TdaClientException(e); } finally { if (response != null && response.code() == 401) { response.close(); } } } private void updateAuthToken() { RequestBody formBody = new FormBody.Builder() .add(AuthToken.GRANT_TYPE_PARAM, AuthToken.GRANT_TYPE_REFRESH) .add(AuthToken.REFRESH_TOKEN_PARAM, tdaProps.getProperty(REFRESH_TOKEN)) .add(AuthToken.CLIENT_ID_PARAM, this.tdaProps.getProperty(CLIENT_ID)) .build(); HttpUrl url = baseUrl() .addPathSegments("oauth2/token") .build(); LOGGER.debug("Attempting to obtain new authentication token using refresh token at {}", url); Request authRequest = new Request.Builder() .url(url) .header("Content-Type", "application/x-www-form-urlencoded") .header("Accept", "application/json") .post(formBody) .build(); try (Response authResponse = httpClient.newCall(authRequest).execute()) { // If the auth failed again, we can't get a new auth token, so we're screwed. checkResponse(authResponse, false); InputStream in = authResponse.body().byteStream(); AuthToken authToken = DefaultMapper.fromJson(in, AuthToken.class); LOGGER.info("new authToken received: {}", authToken); String _accessToken = authToken.getAccessToken(); if (StringUtils.isBlank(_accessToken)) { throw new TdaClientException("Got successful OAuth response, but access token is missing"); } tdaProps.setProperty(ACCESS_TOKEN, _accessToken); } catch (IOException e) { throw new TdaClientException(e); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy