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
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.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.option.OptionChain;
import com.studerw.tda.model.quote.Quote;
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.format.DateTimeFormatter;
import java.util.*;

import okhttp3.Headers;
import okhttp3.HttpUrl;
import okhttp3.HttpUrl.Builder;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 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;
  static final String DEFAULT_PATH = "https://api.tdameritrade.com/v1";
  private static final Logger LOGGER = LoggerFactory.getLogger(HttpTdaClient.class);
  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.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 the tda.token.refresh and tda.client_id. If they * are not set, an exception will be thrown

*/ public HttpTdaClient() { this(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. If they * are not set, an exception will be thrown

* * @param props required properties */ public HttpTdaClient(Properties props) { LOGGER.info("Initiating HttpTdaClient..."); this.tdaProps = (props == null) ? initTdaProps() : props; validateProps(this.tdaProps); this.httpClient = new OkHttpClient.Builder(). cookieJar(new CookieJarImpl(new MemoryCookieStore())). addInterceptor(new OauthInterceptor(this, tdaProps)). addInterceptor(new LoggingInterceptor("TDA_HTTP", Integer.parseInt(tdaProps.getProperty("tda.debug.bytes.length")))). build(); } 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"); } } /** * validates the necessary props like refresh token and client id. 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("tda.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("tda.token.refresh"); 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 url = tdaProps.getProperty("tda.url"); if (StringUtils.isBlank(url)) { tdaProps.setProperty("tda.url", DEFAULT_PATH); } if (tdaProps.get("tda.debug.bytes.length") == null) { tdaProps.setProperty("tda.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.httpClient.newCall(request).execute()) { checkResponse(response, false); return tdaJsonParser.parsePriceHistory(response.body().byteStream()); } catch (IOException e) { throw new RuntimeException(e); } } @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.httpClient.newCall(request).execute()) { checkResponse(response, false); return tdaJsonParser.parsePriceHistory(response.body().byteStream()); } catch (IOException e) { throw new RuntimeException(e); } } @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.httpClient.newCall(request).execute()) { checkResponse(response, false); return tdaJsonParser.parseQuotes(response.body().byteStream()); } catch (IOException e) { throw new RuntimeException(e); } } @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.httpClient.newCall(request).execute()) { checkResponse(response, false); return tdaJsonParser.parseAccount(response.body().byteStream()); } catch (IOException e) { throw new RuntimeException(e); } } @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.httpClient.newCall(request).execute()) { checkResponse(response, false); return tdaJsonParser.parseAccounts(response.body().byteStream()); } catch (IOException e) { throw new RuntimeException(e); } } @Override public void 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.httpClient.newCall(request).execute()) { checkResponse(response, false); if (response.code() != 201) { LOGGER.warn("Expected 201 response, but received " + response.code()); } } catch (IOException e) { throw new RuntimeException(e); } } @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.httpClient.newCall(request).execute()) { checkResponse(response, false); return tdaJsonParser.parseOrders(response.body().byteStream()); } catch (IOException e) { throw new RuntimeException(e); } } @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.httpClient.newCall(request).execute()) { checkResponse(response, false); return tdaJsonParser.parseOrders(response.body().byteStream()); } catch (IOException e) { throw new RuntimeException(e); } } @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.httpClient.newCall(request).execute()) { checkResponse(response, false); return tdaJsonParser.parseOrders(response.body().byteStream()); } catch (IOException e) { throw new RuntimeException(e); } } @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.httpClient.newCall(request).execute()) { checkResponse(response, false); return tdaJsonParser.parseOrder(response.body().byteStream()); } catch (IOException e) { throw new RuntimeException(e); } } @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.httpClient.newCall(request).execute()) { checkResponse(response, false); } catch (IOException e) { throw new RuntimeException(e); } } @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.httpClient.newCall(request).execute()) { checkResponse(response, false); return tdaJsonParser.parseInstrumentMap(response.body().byteStream()); } catch (IOException e) { throw new RuntimeException(e); } } @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.httpClient.newCall(request).execute()) { checkResponse(response, false); final List fullInstruments = tdaJsonParser .parseFullInstrumentMap(response.body().byteStream()); if (fullInstruments.size() != 1) { throw new RuntimeException( "Expecting a single instrument but received: " + fullInstruments.size()); } return fullInstruments.get(0); } catch (IOException e) { throw new RuntimeException(e); } } @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.httpClient.newCall(request).execute()) { checkResponse(response, true); return tdaJsonParser.parseMovers(response.body().byteStream()); } catch (IOException e) { throw new RuntimeException(e); } } @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."); } Builder urlBuilder = baseUrl("marketdata", "chains") .addQueryParameter("symbol", symbol.toUpperCase()); // 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.httpClient.newCall(request).execute()) { checkResponse(response, false); return tdaJsonParser.parseOptionChain(response.body().byteStream()); } catch (IOException e) { throw new RuntimeException(e); } } @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.httpClient.newCall(request).execute()) { checkResponse(response, false); return tdaJsonParser.parseInstrumentArraySingle(response.body().byteStream()); } catch (IOException e) { throw new RuntimeException(e); } } /** * @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 msg = String .format("Non 200 response: [%d - %s] - %s", response.code(), response.message(), response.request().url()); throw new RuntimeException(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 RuntimeException(msg); } } catch (IOException e) { throw new RuntimeException("Error checking for JSON empty body on response"); } } } private Headers defaultHeaders() { Map defaultHeaders = new HashMap<>(); defaultHeaders.put("Accept", "application/json"); // defaultHeaders.put("Accept-Encoding", "gzip"); // 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; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy