com.studerw.tda.client.HttpTdaClient Maven / Gradle / Ivy
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