From baeb5799f14dff70ed8e2adade6fec7b7d40a67d Mon Sep 17 00:00:00 2001 From: Jacob Peterson Date: Mon, 2 Aug 2021 17:18:03 -0600 Subject: [PATCH 01/84] Fix parseAPIErrorResponse so response code and message are included --- .../alpaca/rest/AlpacaClientException.java | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClientException.java b/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClientException.java index 40ef1d6c..bcf89c2b 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClientException.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClientException.java @@ -66,11 +66,7 @@ public String getMessage() { if (responseBody == null) { return super.getMessage(); } else { - try { - return parseAPIErrorResponse(); - } catch (Exception ignored) { - return super.getMessage(); - } + return parseAPIErrorResponse(); } } @@ -87,24 +83,26 @@ public String getMessage() { * @return a formatted message of the error response */ private String parseAPIErrorResponse() { - JsonElement responseJsonElement = JsonParser.parseString(responseBody); + try { + JsonElement responseJsonElement = JsonParser.parseString(responseBody); - if (responseJsonElement instanceof JsonObject) { - JsonObject responseJsonObject = responseJsonElement.getAsJsonObject(); + if (responseJsonElement instanceof JsonObject) { + JsonObject responseJsonObject = responseJsonElement.getAsJsonObject(); - if (responseJsonObject.has(CODE_KEY)) { - apiResponseCode = responseJsonObject.get(CODE_KEY).getAsInt(); - } + if (responseJsonObject.has(CODE_KEY)) { + apiResponseCode = responseJsonObject.get(CODE_KEY).getAsInt(); + } - if (responseJsonObject.has(MESSAGE_KEY)) { - apiResponseMessage = responseJsonObject.get(MESSAGE_KEY).getAsString(); + if (responseJsonObject.has(MESSAGE_KEY)) { + apiResponseMessage = responseJsonObject.get(MESSAGE_KEY).getAsString(); + } } - } - // Just use the response JSON if the message could not be parsed. - if (apiResponseMessage == null) { - apiResponseMessage = responseJsonElement.toString(); - } + // Just use the response JSON if the message could not be parsed. + if (apiResponseMessage == null) { + apiResponseMessage = responseJsonElement.toString(); + } + } catch (Exception ignored) {} // Build message StringBuilder messageBuilder = new StringBuilder(); From 997ec27ba85a9cf8b6dd4c7740ffc274bda17b8f Mon Sep 17 00:00:00 2001 From: Jacob Peterson Date: Tue, 3 Aug 2021 16:44:58 -0600 Subject: [PATCH 02/84] Fix NPE if no listener is set in AlpacaWebsocket --- .../alpaca/websocket/AlpacaWebsocket.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java index 19dee225..331d89da 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java @@ -259,10 +259,12 @@ public Future getAuthorizationFuture() { * @param message the message */ protected void callListener(T messageType, M message) { - try { - listener.onMessage(messageType, message); - } catch (Exception exception) { - LOGGER.error("{} listener threw exception!", websocketName, exception); + if (listener != null) { + try { + listener.onMessage(messageType, message); + } catch (Exception exception) { + LOGGER.error("{} listener threw exception!", websocketName, exception); + } } } From fe54b204c965aa5372f6bf1e52687a79c823a4eb Mon Sep 17 00:00:00 2001 From: cloudygeek Date: Wed, 4 Aug 2021 23:13:46 +0100 Subject: [PATCH 03/84] Update FormatUtil.java TimeZone Bug --- .../net/jacobpeterson/alpaca/util/format/FormatUtil.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/java/net/jacobpeterson/alpaca/util/format/FormatUtil.java b/src/main/java/net/jacobpeterson/alpaca/util/format/FormatUtil.java index 0b76f3b5..d2bf5b82 100644 --- a/src/main/java/net/jacobpeterson/alpaca/util/format/FormatUtil.java +++ b/src/main/java/net/jacobpeterson/alpaca/util/format/FormatUtil.java @@ -22,9 +22,6 @@ public class FormatUtil { // the 3rd and 4th least significant decimal digits. private static final NumberFormat CURRENCY_FORMATTER = new DecimalFormat("#0.00##", DecimalFormatSymbols.getInstance(Locale.US)); - private static final DateTimeFormatter RFC_3339_FORMATTER = - DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(NEW_YORK_ZONED_ID); - /** * Formats an arbitrary number to a currency format. e.g. $123.45 * @@ -37,13 +34,15 @@ public static String toCurrencyFormat(Number numberToFormat) { } /** - * Formats a {@link TemporalAccessor} using {@link #RFC_3339_FORMATTER} {@link DateTimeFormatter}. + * Formats a {@link TemporalAccessor} using {@link DateTimeFormatter#ISO_OFFSET_DATE_TIME}. * * @param zonedDateTime the {@link ZonedDateTime} * * @return the formatted string */ public static String toRFC3339Format(ZonedDateTime zonedDateTime) { - return RFC_3339_FORMATTER.format(zonedDateTime); + //Alpaca requires RFC 3339 (https://www.ietf.org/rfc/rfc3339.txt) date time to be provided which includes time zone. + // ISO 8601 is compatible with RFC 3339. + return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(zonedDateTime); } } From 206774fe8b3878081801e455b108b187f4722411 Mon Sep 17 00:00:00 2001 From: Jacob Peterson Date: Thu, 5 Aug 2021 11:04:58 -0600 Subject: [PATCH 04/84] Add 'n' and 'vw' fields to Bar --- schema_json/endpoint/market_data/historical/bar/bar.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/schema_json/endpoint/market_data/historical/bar/bar.json b/schema_json/endpoint/market_data/historical/bar/bar.json index b39bff53..6be1fe66 100644 --- a/schema_json/endpoint/market_data/historical/bar/bar.json +++ b/schema_json/endpoint/market_data/historical/bar/bar.json @@ -25,6 +25,14 @@ "v": { "existingJavaType": "java.lang.Long", "title": "Volume." + }, + "n": { + "existingJavaType": "java.lang.Long", + "title": "Trade count." + }, + "vw": { + "existingJavaType": "java.lang.Double", + "title": "VWAP (Volume Weighted Average Price)." } } } From 23f126f257daff669f82ff9076d52e277c4d97b6 Mon Sep 17 00:00:00 2001 From: Jacob Peterson Date: Thu, 5 Aug 2021 11:17:30 -0600 Subject: [PATCH 05/84] Fix Javadoc and formatting in FormatUtil --- .../net/jacobpeterson/alpaca/util/format/FormatUtil.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/jacobpeterson/alpaca/util/format/FormatUtil.java b/src/main/java/net/jacobpeterson/alpaca/util/format/FormatUtil.java index d2bf5b82..b5a53a27 100644 --- a/src/main/java/net/jacobpeterson/alpaca/util/format/FormatUtil.java +++ b/src/main/java/net/jacobpeterson/alpaca/util/format/FormatUtil.java @@ -22,6 +22,7 @@ public class FormatUtil { // the 3rd and 4th least significant decimal digits. private static final NumberFormat CURRENCY_FORMATTER = new DecimalFormat("#0.00##", DecimalFormatSymbols.getInstance(Locale.US)); + /** * Formats an arbitrary number to a currency format. e.g. $123.45 * @@ -35,14 +36,16 @@ public static String toCurrencyFormat(Number numberToFormat) { /** * Formats a {@link TemporalAccessor} using {@link DateTimeFormatter#ISO_OFFSET_DATE_TIME}. + *
+ * Alpaca requires RFC 3339 formatted timestamps to be provided which includes the timezone. ISO + * 8601 is compatible with RFC 3339 which is what {@link DateTimeFormatter#ISO_OFFSET_DATE_TIME} + * uses. * * @param zonedDateTime the {@link ZonedDateTime} * * @return the formatted string */ public static String toRFC3339Format(ZonedDateTime zonedDateTime) { - //Alpaca requires RFC 3339 (https://www.ietf.org/rfc/rfc3339.txt) date time to be provided which includes time zone. - // ISO 8601 is compatible with RFC 3339. return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(zonedDateTime); } } From 945bed160093150dfce957905f5e573188fdb689 Mon Sep 17 00:00:00 2001 From: Jacob Peterson Date: Thu, 5 Aug 2021 11:56:38 -0600 Subject: [PATCH 06/84] Modify websocket Javadocs --- .../websocket/AlpacaWebsocketInterface.java | 7 +++++-- .../MarketDataWebsocketInterface.java | 18 +++++++++--------- .../streaming/StreamingWebsocketInterface.java | 4 ++-- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketInterface.java b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketInterface.java index c508e799..272774f6 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketInterface.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketInterface.java @@ -39,14 +39,17 @@ public interface AlpacaWebsocketInterface + * Note that if this {@link AlpacaWebsocketInterface} is already authorized, the returned {@link Future} will likely + * never complete. * * @return a {@link Boolean} {@link Future} */ Future getAuthorizationFuture(); /** - * Waits for {@link #getAuthorizationFuture()} to complete and returns its value. This will timeout after 10 seconds - * and then return false. + * Waits for {@link #getAuthorizationFuture()} to complete and returns its value. After 10 seconds of waiting, this + * will timeout then return false. * * @return a boolean */ diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocketInterface.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocketInterface.java index 7c7d4d90..2170e898 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocketInterface.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocketInterface.java @@ -15,11 +15,11 @@ public interface MarketDataWebsocketInterface extends AlpacaWebsocketInterface { /** - * Subscribe to a specific control {@link MarketDataMessage}. That is, {@link ErrorMessage}, {@link - * SubscriptionsMessage}, or {@link SuccessMessage}. + * Subscribe to a specific control {@link MarketDataMessage} which contain information about the stream's current + * state. That is, an {@link ErrorMessage}, {@link SubscriptionsMessage}, or {@link SuccessMessage}. * - * @param marketDataMessageTypes array of any of: {@link MarketDataMessageType#SUCCESS}, {@link - * MarketDataMessageType#ERROR}, or {@link MarketDataMessageType#SUBSCRIPTION} + * @param marketDataMessageTypes array containing any of the following: {@link MarketDataMessageType#SUCCESS}, + * {@link MarketDataMessageType#ERROR}, or {@link MarketDataMessageType#SUBSCRIPTION} */ void subscribeToControl(MarketDataMessageType... marketDataMessageTypes); @@ -44,7 +44,7 @@ public interface MarketDataWebsocketInterface extends AlpacaWebsocketInterface - * Note that any one of the given {@link Collection}s can contain the wildcard character e.g. "*" to unsubscribe + * Note that any one of the given {@link Collection}s can contain the wildcard character (e.g. "*") to unsubscribe * from a previously subscribed wildcard. * * @param tradeSymbols a {@link Collection} of symbols to unsubscribe from trades or null for no @@ -58,28 +58,28 @@ public interface MarketDataWebsocketInterface extends AlpacaWebsocketInterface tradeSymbols, Collection quoteSymbols, Collection barSymbols); /** - * Gets all of the currently subscribed control {@link MarketDataMessageType}s. + * Gets all the currently subscribed control {@link MarketDataMessageType}s. * * @return a {@link Collection} of {@link MarketDataMessageType}s */ Collection subscribedControls(); /** - * Gets all of the currently subscribed symbols for trade updates. + * Gets all the currently subscribed symbols for trade updates. * * @return a {@link Collection} of {@link String}s */ Collection subscribedTrades(); /** - * Gets all of the currently subscribed symbols for quote updates. + * Gets all the currently subscribed symbols for quote updates. * * @return a {@link Collection} of {@link String}s */ Collection subscribedQuotes(); /** - * Gets all of the currently subscribed symbols for bar updates. + * Gets all the currently subscribed symbols for bar updates. * * @return a {@link Collection} of {@link String}s */ diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocketInterface.java b/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocketInterface.java index 9b2dc5ca..07112a68 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocketInterface.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocketInterface.java @@ -11,7 +11,7 @@ public interface StreamingWebsocketInterface extends AlpacaWebsocketInterface { /** - * Sets the {@link StreamingMessageType}s to streamingMessageTypes. + * Sets the {@link StreamingMessageType}s for this stream to the given streamingMessageTypes. *
* Note that this will call {@link StreamingWebsocketInterface#connect()} and {@link * StreamingWebsocketInterface#waitForAuthorization()} if {@link StreamingWebsocketInterface#isConnected()} returns @@ -22,7 +22,7 @@ public interface StreamingWebsocketInterface extends AlpacaWebsocketInterface Date: Thu, 5 Aug 2021 12:04:42 -0600 Subject: [PATCH 07/84] Add websocket clarifications to README --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index dad90b54..f144c7d6 100644 --- a/README.md +++ b/README.md @@ -384,9 +384,13 @@ alpacaAPI.streaming().streams( The following methods show how you can control the state of the [`StreamingWebsocket`](src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocket.java) directly. ```java +// Note that you usually won't need to call connect() and waitForAuthorization() +// directly, as they are automatically called when subscribing to streams. alpacaAPI.streaming().connect(); alpacaAPI.streaming().waitForAuthorization(); System.out.println(alpacaAPI.streaming().isValid()); + +// You can manually disconnect the websocket as needed alpacaAPI.streaming().disconnect(); ``` @@ -417,9 +421,13 @@ alpacaAPI.marketDataStreaming().subscribe( The following methods show how you can control the state of the [`MarketDataWebsocket`](src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java) directly. ```java +// Note that you usually won't need to call connect() and waitForAuthorization() +// directly, as they are automatically called when subscribing to streams. alpacaAPI.marketDataStreaming().connect(); alpacaAPI.marketDataStreaming().waitForAuthorization(); System.out.println(alpacaAPI.marketDataStreaming().isValid()); + +// You can manually disconnect the websocket as needed alpacaAPI.marketDataStreaming().disconnect(); ``` From 7986444a31b5f3f6cb2664e679392140457b9fe0 Mon Sep 17 00:00:00 2001 From: Jacob Peterson Date: Thu, 5 Aug 2021 13:35:06 -0600 Subject: [PATCH 08/84] Remove auto-connect feature from websockets --- README.md | 56 ++++++++++--------- .../websocket/AlpacaWebsocketInterface.java | 11 ++-- .../marketdata/MarketDataWebsocket.java | 10 +--- .../MarketDataWebsocketInterface.java | 4 -- .../streaming/StreamingWebsocket.java | 14 ++--- .../StreamingWebsocketInterface.java | 4 -- 6 files changed, 44 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index f144c7d6..b0c47a8d 100644 --- a/README.md +++ b/README.md @@ -374,28 +374,31 @@ StreamingListener streamingListener = (messageType, message) -> System.out.printf("%s: %s\n", messageType.name(), message); alpacaAPI.streaming().setListener(streamingListener); -// Listen to the 'authorization' and 'trade update' streams. -// Note this will connect the websocket and wait for authorization -// if it isn't already connected. -alpacaAPI.streaming().streams( - StreamingMessageType.AUTHORIZATION, - StreamingMessageType.TRADE_UPDATES); -``` +// Listen 'AuthorizationMessage' and 'ListeningMessage' messages that contain +// information about the stream's current state. +alpacaAPI.streaming().streams(StreamingMessageType.AUTHORIZATION, + StreamingMessageType.LISTENING); -The following methods show how you can control the state of the [`StreamingWebsocket`](src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocket.java) directly. -```java -// Note that you usually won't need to call connect() and waitForAuthorization() -// directly, as they are automatically called when subscribing to streams. +// Connect the websocket and confirm authentication alpacaAPI.streaming().connect(); -alpacaAPI.streaming().waitForAuthorization(); -System.out.println(alpacaAPI.streaming().isValid()); +alpacaAPI.streaming().waitForAuthorization(5, TimeUnit.SECONDS); +if (!alpacaAPI.streaming().isValid()) { + System.out.println("Websocket not valid!"); + return; +} -// You can manually disconnect the websocket as needed +// Listen to the 'trade update' streams. +alpacaAPI.streaming().streams(StreamingMessageType.TRADE_UPDATES); + +// Wait a few seconds +Thread.sleep(5000); + +// Manually disconnect the websocket alpacaAPI.streaming().disconnect(); ``` ## [`MarketDataWebsocket`](src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java) -Alpaca's Data API v2 provides websocket streaming for trades, quotes, and minute bars. This helps receive the most up to date market information that could help your trading strategy to act upon certain market movement. +Alpaca's Data API v2 provides websocket streaming for trades, quotes, and minute bars. This helps receive the most up-to-date market information that could help your trading strategy to act upon certain market movement. Example usage: ```java @@ -410,24 +413,25 @@ alpacaAPI.marketDataStreaming().subscribeToControl( MarketDataMessageType.SUCCESS, MarketDataMessageType.SUBSCRIPTION, MarketDataMessageType.ERROR); + +// Connect the websocket and confirm authentication +alpacaAPI.marketDataStreaming().connect(); +alpacaAPI.marketDataStreaming().waitForAuthorization(5, TimeUnit.SECONDS); +if (!alpacaAPI.marketDataStreaming().isValid()) { + System.out.println("Websocket not valid!"); + return; +} + // Listen to the AAPL and TSLA trades and all ('*') bars. -// Note this will connect the websocket and wait for authorization -// if it isn't already connected. alpacaAPI.marketDataStreaming().subscribe( Arrays.asList("AAPL", "TSLA"), null, Arrays.asList("*")); -``` -The following methods show how you can control the state of the [`MarketDataWebsocket`](src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java) directly. -```java -// Note that you usually won't need to call connect() and waitForAuthorization() -// directly, as they are automatically called when subscribing to streams. -alpacaAPI.marketDataStreaming().connect(); -alpacaAPI.marketDataStreaming().waitForAuthorization(); -System.out.println(alpacaAPI.marketDataStreaming().isValid()); +// Wait a few seconds +Thread.sleep(5000); -// You can manually disconnect the websocket as needed +// Manually disconnect the websocket alpacaAPI.marketDataStreaming().disconnect(); ``` diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketInterface.java b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketInterface.java index 272774f6..e7d8b60c 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketInterface.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketInterface.java @@ -48,14 +48,17 @@ public interface AlpacaWebsocketInterface getAuthorizationFuture(); /** - * Waits for {@link #getAuthorizationFuture()} to complete and returns its value. After 10 seconds of waiting, this - * will timeout then return false. + * Waits for {@link #getAuthorizationFuture()} to complete and returns its value, except when timeout + * time has elapsed, then this will return false. + * + * @param timeout the timeout time + * @param unit the timeout {@link TimeUnit} * * @return a boolean */ - default boolean waitForAuthorization() { + default boolean waitForAuthorization(long timeout, TimeUnit unit) { try { - return getAuthorizationFuture().get(10, TimeUnit.SECONDS); + return getAuthorizationFuture().get(timeout, unit); } catch (InterruptedException | ExecutionException | TimeoutException ignored) {} return false; } diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java index ad189169..48953560 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java @@ -27,6 +27,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.TimeUnit; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; @@ -107,7 +108,7 @@ protected void onConnection() { @Override protected void onReconnection() { sendAuthenticationMessage(); - if (waitForAuthorization()) { + if (waitForAuthorization(5, TimeUnit.SECONDS)) { subscribeToControl(Iterables.toArray(listenedMarketDataMessageTypes, MarketDataMessageType.class)); subscribe(subscribedTrades, subscribedQuotes, subscribedBars); } @@ -292,12 +293,7 @@ public void unsubscribe(Collection tradeSymbols, Collection quot private void sendSubscriptionUpdate(Collection tradeSymbols, Collection quoteSymbols, Collection barSymbols, boolean subscribe) { if (!isConnected()) { - connect(); - - if (!waitForAuthorization()) { - LOGGER.error("Not subscribing to streams due to unauthorized {} websocket.", websocketName); - return; - } + throw new IllegalStateException("This websocket must be connected before sending subscription updates!"); } /* Format of message is: diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocketInterface.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocketInterface.java index 2170e898..9ac90112 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocketInterface.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocketInterface.java @@ -28,10 +28,6 @@ public interface MarketDataWebsocketInterface extends AlpacaWebsocketInterface * Note that any one of the given {@link Collection}s can contain the wildcard character e.g. "*" to subscribe to * ALL available symbols. - *
- * Note that this will call {@link MarketDataWebsocketInterface#connect()} and {@link - * MarketDataWebsocketInterface#waitForAuthorization()} if {@link MarketDataWebsocketInterface#isConnected()} - * returns false. * * @param tradeSymbols a {@link Collection} of symbols to subscribe to trades or null for no change * @param quoteSymbols a {@link Collection} of symbols to subscribe to quotes or null for no change diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocket.java index 8c52b078..57ede8a4 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocket.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocket.java @@ -27,6 +27,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.TimeUnit; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; @@ -94,7 +95,7 @@ protected void onConnection() { @Override protected void onReconnection() { sendAuthenticationMessage(); - if (waitForAuthorization()) { + if (waitForAuthorization(5, TimeUnit.SECONDS)) { streams(Iterables.toArray(listenedStreamMessageTypes, StreamingMessageType.class)); } } @@ -214,15 +215,6 @@ public void streams(StreamingMessageType... streamingMessageTypes) { .filter(not(SUBSCRIBABLE_STREAMING_MESSAGE_TYPES::contains)) .forEach(listenedStreamMessageTypes::add); - if (!isConnected()) { - connect(); - - if (!waitForAuthorization()) { - LOGGER.error("Not subscribing to streams due to unauthorized {} websocket.", websocketName); - return; - } - } - // Stream request format: // { // "action": "listen", @@ -241,6 +233,8 @@ public void streams(StreamingMessageType... streamingMessageTypes) { if (streamsArray.isEmpty()) { return; + } else if (!isConnected()) { + throw new IllegalStateException("This websocket must be connected before subscribing to streams!"); } JsonObject dataObject = new JsonObject(); diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocketInterface.java b/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocketInterface.java index 07112a68..44f0042e 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocketInterface.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocketInterface.java @@ -12,10 +12,6 @@ public interface StreamingWebsocketInterface extends AlpacaWebsocketInterfacestreamingMessageTypes. - *
- * Note that this will call {@link StreamingWebsocketInterface#connect()} and {@link - * StreamingWebsocketInterface#waitForAuthorization()} if {@link StreamingWebsocketInterface#isConnected()} returns - * false. * * @param streamingMessageTypes the {@link StreamingMessageType}s */ From 2a95a4911f3a0eedd3a8c9b67a07d79f9b059b3f Mon Sep 17 00:00:00 2001 From: Jacob Peterson Date: Thu, 5 Aug 2021 14:17:24 -0600 Subject: [PATCH 09/84] Add websocket clarification to README --- README.md | 8 ++++++-- .../websocket/AlpacaWebsocketInterface.java | 18 +++++++++--------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index b0c47a8d..cca3aa50 100644 --- a/README.md +++ b/README.md @@ -375,7 +375,9 @@ StreamingListener streamingListener = (messageType, message) -> alpacaAPI.streaming().setListener(streamingListener); // Listen 'AuthorizationMessage' and 'ListeningMessage' messages that contain -// information about the stream's current state. +// information about the stream's current state. Note that these are subscribed +// to before the websocket is connected since these messages usually are sent +// upon websocket connection. alpacaAPI.streaming().streams(StreamingMessageType.AUTHORIZATION, StreamingMessageType.LISTENING); @@ -408,7 +410,9 @@ MarketDataListener marketDataListener = (messageType, message) -> alpacaAPI.marketDataStreaming().setListener(marketDataListener); // Listen to 'SubscriptionsMessage', 'SuccessMessage', and 'ErrorMessage' control messages -// that contain information about the stream's current state. +// that contain information about the stream's current state. Note that these are subscribed +// to before the websocket is connected since these messages usually are sent +// upon websocket connection. alpacaAPI.marketDataStreaming().subscribeToControl( MarketDataMessageType.SUCCESS, MarketDataMessageType.SUBSCRIPTION, diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketInterface.java b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketInterface.java index e7d8b60c..f524e1df 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketInterface.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketInterface.java @@ -36,6 +36,15 @@ public interface AlpacaWebsocketInterface Date: Thu, 5 Aug 2021 14:31:58 -0600 Subject: [PATCH 10/84] Update to 8.2 --- README.md | 4 ++-- build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cca3aa50..e56eab73 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ If you are using Gradle as your build tool, add the following dependency to your ``` dependencies { - implementation group: 'net.jacobpeterson', name: 'alpaca-java', version: '8.1' + implementation group: 'net.jacobpeterson', name: 'alpaca-java', version: '8.2' } ``` @@ -27,7 +27,7 @@ If you are using Maven as your build tool, add the following dependency to your net.jacobpeterson alpaca-java - 8.1 + 8.2 compile ``` diff --git a/build.gradle b/build.gradle index 6e25af5e..cbace30e 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,7 @@ tasks.getByName("generateJsonSchema2Pojo").enabled(false) // Disable default 'js archivesBaseName = 'alpaca-java' group = 'net.jacobpeterson' -version = '8.1-SNAPSHOT' +version = '8.2-SNAPSHOT' repositories { mavenCentral() From 9f6a5493a27381fd76512bec5b48bc7ad9ec3ab4 Mon Sep 17 00:00:00 2001 From: Jacob Peterson Date: Mon, 13 Sep 2021 15:24:52 -0600 Subject: [PATCH 11/84] Add BarAdjustment and BarTimePeriod enums to getBars() --- README.md | 6 ++- .../historical/bar/enums/bar_adjustment.json | 24 ++++++++++++ ...s_time_frame.json => bar_time_period.json} | 12 +++--- .../rest/endpoint/MarketDataEndpoint.java | 39 ++++++++++++------- .../rest/endpoint/PositionsEndpoint.java | 7 +++- 5 files changed, 65 insertions(+), 23 deletions(-) create mode 100644 schema_json/endpoint/market_data/historical/bar/enums/bar_adjustment.json rename schema_json/endpoint/market_data/historical/bar/enums/{bars_time_frame.json => bar_time_period.json} (68%) diff --git a/README.md b/README.md index e56eab73..7886cd50 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ The Data API v2 provides market data through an easy-to-use API for historical d Example usage: ```java try { - // Get AAPL one hour bars from 7/6/2021 market open + // Get AAPL one hour, split-adjusted bars from 7/6/2021 market open // to 7/8/2021 market close and print them out BarsResponse aaplBarsResponse = alpacaAPI.marketData().getBars( "AAPL", @@ -109,7 +109,9 @@ try { ZonedDateTime.of(2021, 7, 8, 12 + 4, 0, 0, 0, ZoneId.of("America/New_York")), null, null, - BarsTimeFrame.ONE_HOUR); + 1, + BarTimePeriod.HOUR, + BarAdjustment.SPLIT); aaplBarsResponse.getBars().forEach(System.out::println); // Get AAPL first 10 trades on 7/8/2021 at market open and print them out diff --git a/schema_json/endpoint/market_data/historical/bar/enums/bar_adjustment.json b/schema_json/endpoint/market_data/historical/bar/enums/bar_adjustment.json new file mode 100644 index 00000000..4f28b6ed --- /dev/null +++ b/schema_json/endpoint/market_data/historical/bar/enums/bar_adjustment.json @@ -0,0 +1,24 @@ +{ + "type": "string", + "title": "See Historical Data.", + "enum": [ + "raw", + "split", + "dividend", + "all" + ], + "javaEnums": [ + { + "name": "RAW" + }, + { + "name": "SPLIT" + }, + { + "name": "DIVIDEND" + }, + { + "name": "ALL" + } + ] +} diff --git a/schema_json/endpoint/market_data/historical/bar/enums/bars_time_frame.json b/schema_json/endpoint/market_data/historical/bar/enums/bar_time_period.json similarity index 68% rename from schema_json/endpoint/market_data/historical/bar/enums/bars_time_frame.json rename to schema_json/endpoint/market_data/historical/bar/enums/bar_time_period.json index 89eb394f..627d9413 100644 --- a/schema_json/endpoint/market_data/historical/bar/enums/bars_time_frame.json +++ b/schema_json/endpoint/market_data/historical/bar/enums/bar_time_period.json @@ -2,19 +2,19 @@ "type": "string", "title": "See Historical Data.", "enum": [ - "1Min", - "1Hour", - "1Day" + "Min", + "Hour", + "Day" ], "javaEnums": [ { - "name": "ONE_MINUTE" + "name": "MINUTE" }, { - "name": "ONE_HOUR" + "name": "HOUR" }, { - "name": "ONE_DAY" + "name": "DAY" } ] } diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/MarketDataEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/MarketDataEndpoint.java index 9a270e21..a1b4c425 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/MarketDataEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/MarketDataEndpoint.java @@ -3,7 +3,8 @@ import com.google.gson.reflect.TypeToken; import net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.bar.Bar; import net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.bar.BarsResponse; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.bar.enums.BarsTimeFrame; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.bar.enums.BarAdjustment; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.bar.enums.BarTimePeriod; import net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.quote.LatestQuoteResponse; import net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.quote.Quote; import net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.quote.QuotesResponse; @@ -180,26 +181,34 @@ public LatestQuoteResponse getLatestQuote(String symbol) throws AlpacaClientExce /** * Gets {@link Bar} aggregate historical data for the requested security. * - * @param symbol the symbol to query for - * @param start filter data equal to or after this {@link ZonedDateTime}. Fractions of a second are not - * accepted. - * @param end filter data equal to or before this {@link ZonedDateTime}. Fractions of a second are not - * accepted. - * @param limit number of data points to return. Must be in range 1-10000, defaults to 1000 if null - * is given - * @param pageToken pagination token to continue from - * @param timeFrame the {@link BarsTimeFrame} for the aggregation + * @param symbol the symbol to query for + * @param start filter data equal to or after this {@link ZonedDateTime}. Fractions of a second are + * not accepted. + * @param end filter data equal to or before this {@link ZonedDateTime}. Fractions of a second are + * not accepted. + * @param limit number of data points to return. Must be in range 1-10000, defaults to 1000 if + * null is given + * @param pageToken pagination token to continue from + * @param barTimePeriodDuration the duration for the given barTimePeriod parameter. e.g. for + * 15Min bars, you would supply 15 for this parameter and + * {@link BarTimePeriod#MINUTE} for the barTimePeriod parameter. + * @param barTimePeriod the {@link BarTimePeriod} e.g. for 15Min bars, you would supply {@link + * BarTimePeriod#MINUTE} for this parameter and 15 for the + * barTimePeriodDuration parameter. + * @param barAdjustment specifies the corporate action adjustment for the stocks. Default value is {@link + * BarAdjustment#RAW}. * * @return the {@link BarsResponse} * * @throws AlpacaClientException thrown for {@link AlpacaClientException}s */ public BarsResponse getBars(String symbol, ZonedDateTime start, ZonedDateTime end, Integer limit, - String pageToken, BarsTimeFrame timeFrame) throws AlpacaClientException { + String pageToken, int barTimePeriodDuration, BarTimePeriod barTimePeriod, BarAdjustment barAdjustment) + throws AlpacaClientException { checkNotNull(symbol); checkNotNull(start); checkNotNull(end); - checkNotNull(timeFrame); + checkNotNull(barTimePeriod); HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() .addPathSegment(endpointPathSegment) @@ -208,7 +217,7 @@ public BarsResponse getBars(String symbol, ZonedDateTime start, ZonedDateTime en urlBuilder.addQueryParameter("start", FormatUtil.toRFC3339Format(start)); urlBuilder.addQueryParameter("end", FormatUtil.toRFC3339Format(end)); - urlBuilder.addQueryParameter("timeframe", timeFrame.toString()); + urlBuilder.addQueryParameter("timeframe", barTimePeriodDuration + barTimePeriod.toString()); if (limit != null) { urlBuilder.addQueryParameter("limit", limit.toString()); @@ -218,6 +227,10 @@ public BarsResponse getBars(String symbol, ZonedDateTime start, ZonedDateTime en urlBuilder.addQueryParameter("page_token", pageToken); } + if (barAdjustment != null) { + urlBuilder.addQueryParameter("adjustment", barAdjustment.toString()); + } + Request request = alpacaClient.requestBuilder(urlBuilder.build()) .get() .build(); diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/PositionsEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/PositionsEndpoint.java index 676537ba..26d72861 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/PositionsEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/PositionsEndpoint.java @@ -13,7 +13,6 @@ import java.util.ArrayList; import java.util.List; -import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; /** @@ -98,6 +97,11 @@ public List closeAll(Boolean cancelOrders) throws AlpacaClie * positions. * * @param symbolOrAssetID the symbol or {@link Asset#getId()} + * @param quantity the number of shares to liquidate. Can accept up to 9 decimal points. Cannot + * work with percentage. + * @param percentage the percentage of the position to liquidate. Must be between 0 and + * 100. Would only sell fractional if position is originally fractional. Can + * accept up to 9 decimal points. Cannot work with quantity. * * @return a closing {@link Position} {@link Order} * @@ -105,7 +109,6 @@ public List closeAll(Boolean cancelOrders) throws AlpacaClie */ public Order close(String symbolOrAssetID, Integer quantity, Double percentage) throws AlpacaClientException { checkNotNull(symbolOrAssetID); - checkArgument(quantity != null ^ percentage != null, "Either 'quantity' or 'percentage' are required."); HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() .addPathSegment(endpointPathSegment) From 1d7609275498816d2e97a7310b3ff853600196e3 Mon Sep 17 00:00:00 2001 From: Jacob Peterson Date: Mon, 13 Sep 2021 15:26:13 -0600 Subject: [PATCH 12/84] Update to 8.3 --- README.md | 4 ++-- build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7886cd50..946e09d6 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ If you are using Gradle as your build tool, add the following dependency to your ``` dependencies { - implementation group: 'net.jacobpeterson', name: 'alpaca-java', version: '8.2' + implementation group: 'net.jacobpeterson', name: 'alpaca-java', version: '8.3' } ``` @@ -27,7 +27,7 @@ If you are using Maven as your build tool, add the following dependency to your net.jacobpeterson alpaca-java - 8.2 + 8.3 compile ``` diff --git a/build.gradle b/build.gradle index cbace30e..1eb30114 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,7 @@ tasks.getByName("generateJsonSchema2Pojo").enabled(false) // Disable default 'js archivesBaseName = 'alpaca-java' group = 'net.jacobpeterson' -version = '8.2-SNAPSHOT' +version = '8.3-SNAPSHOT' repositories { mavenCentral() From 3064fa3d51298f355c966aadb7fe88c390dc0f0e Mon Sep 17 00:00:00 2001 From: Jacob Peterson Date: Mon, 20 Sep 2021 08:11:41 -0600 Subject: [PATCH 13/84] Fix getSnapshot path segment typo --- .../jacobpeterson/alpaca/rest/endpoint/MarketDataEndpoint.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/MarketDataEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/MarketDataEndpoint.java index a1b4c425..45fc4a08 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/MarketDataEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/MarketDataEndpoint.java @@ -279,7 +279,7 @@ public Snapshot getSnapshot(String symbol) throws AlpacaClientException { HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() .addPathSegment(endpointPathSegment) .addPathSegment(symbol) - .addPathSegment("shapshot"); + .addPathSegment("snapshot"); Request request = alpacaClient.requestBuilder(urlBuilder.build()) .get() From 9bceea3171eee8078549d02f30138b18ef729dc0 Mon Sep 17 00:00:00 2001 From: Jacob Peterson Date: Mon, 20 Sep 2021 08:12:54 -0600 Subject: [PATCH 14/84] Update to 8.3.1 --- README.md | 4 ++-- build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 946e09d6..b8ac95e1 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ If you are using Gradle as your build tool, add the following dependency to your ``` dependencies { - implementation group: 'net.jacobpeterson', name: 'alpaca-java', version: '8.3' + implementation group: 'net.jacobpeterson', name: 'alpaca-java', version: '8.3.1' } ``` @@ -27,7 +27,7 @@ If you are using Maven as your build tool, add the following dependency to your net.jacobpeterson alpaca-java - 8.3 + 8.3.1 compile ``` diff --git a/build.gradle b/build.gradle index 1eb30114..d5566da8 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,7 @@ tasks.getByName("generateJsonSchema2Pojo").enabled(false) // Disable default 'js archivesBaseName = 'alpaca-java' group = 'net.jacobpeterson' -version = '8.3-SNAPSHOT' +version = '8.3.1-SNAPSHOT' repositories { mavenCentral() From 5df5e105706f7135df28255f1019137d8db93e52 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Tue, 12 Oct 2021 17:20:56 -0600 Subject: [PATCH 15/84] Update README, cleanup AlpacaAPITest --- README.md | 3 +- .../alpaca/test/live/AlpacaAPITest.java | 41 +++++++++++-------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index b8ac95e1..f571e41e 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ data_api_type = The default values for `alpaca.properties` can be found [here](src/main/resources/alpaca.default.properties). # Logger -For logging, this library uses [SLF4j](http://www.slf4j.org/) which serves as an interface for various logging frameworks. This enables you to use whatever logging framework you would like. However, if you do not add a logging framework as a dependency in your project, the console will output a message stating that SLF4j is defaulting to a no-operation (NOP) logger implementation. To enable logging, add a logging framework of your choice as a dependency to your project such as [Log4j 2](http://logging.apache.org/log4j/2.x/index.html), [SLF4j-simple](http://www.slf4j.org/manual.html), or [Apache Commons Logging](https://commons.apache.org/proper/commons-logging/). +For logging, this library uses [SLF4j](http://www.slf4j.org/) which serves as an interface for various logging frameworks. This enables you to use whatever logging framework you would like. However, if you do not add a logging framework as a dependency in your project, the console will output a message stating that SLF4j is defaulting to a no-operation (NOP) logger implementation. To enable logging, add a logging framework of your choice as a dependency to your project such as [Logback](http://logback.qos.ch/), [Log4j 2](http://logging.apache.org/log4j/2.x/index.html), [SLF4j-simple](http://www.slf4j.org/manual.html), or [Apache Commons Logging](https://commons.apache.org/proper/commons-logging/). # Examples Note that the examples below are not exhaustive. Refer to the [Javadoc](https://javadoc.io/doc/net.jacobpeterson/alpaca-java) for all classes and method signatures. @@ -471,6 +471,7 @@ Note that the live tests will modify your account minimally. It's meant to test - Use [TA4j](https://github.com/ta4j/ta4j) `Num` interface instead of `Double` for number variables so that users can use either `Double` or `BigDecimal` for performance or precision in price data. - Add [TimeSeriesDataStore](https://github.com/Petersoj/TimeSeriesDataStore) using Alpaca Data API +# Contributing Contributions are welcome! If you are creating a Pull Request, be sure to create a new branch in your forked repository for your feature or bug fix instead of committing directly to the `master` branch in your fork. diff --git a/src/test/java/net/jacobpeterson/alpaca/test/live/AlpacaAPITest.java b/src/test/java/net/jacobpeterson/alpaca/test/live/AlpacaAPITest.java index 00fe9e96..326a6aaf 100644 --- a/src/test/java/net/jacobpeterson/alpaca/test/live/AlpacaAPITest.java +++ b/src/test/java/net/jacobpeterson/alpaca/test/live/AlpacaAPITest.java @@ -1,5 +1,6 @@ package net.jacobpeterson.alpaca.test.live; +import net.jacobpeterson.alpaca.AlpacaAPI; import net.jacobpeterson.alpaca.model.endpoint.account.Account; import net.jacobpeterson.alpaca.model.endpoint.accountactivities.AccountActivity; import net.jacobpeterson.alpaca.model.endpoint.accountactivities.NonTradeActivity; @@ -12,21 +13,28 @@ import net.jacobpeterson.alpaca.model.endpoint.common.enums.SortDirection; import net.jacobpeterson.alpaca.model.endpoint.order.Order; import net.jacobpeterson.alpaca.model.endpoint.order.enums.CurrentOrderStatus; -import net.jacobpeterson.alpaca.AlpacaAPI; import net.jacobpeterson.alpaca.rest.AlpacaClientException; +import net.jacobpeterson.alpaca.rest.endpoint.AccountActivitiesEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.AccountConfigurationEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.AccountEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.ClockEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.AccountActivitiesEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.OrdersEndpoint; -import org.junit.jupiter.api.*; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.time.ZonedDateTime; +import java.util.Collection; import java.util.List; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; /** * {@link AlpacaAPITest} tests live endpoints using Alpaca Paper credentials given in the @@ -35,14 +43,14 @@ @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class AlpacaAPITest { - private static final Logger LOGGER = LoggerFactory.getLogger(AlpacaAPITest.class); - private static final int RATE_LIMIT_MILLIS = 200; // Wait 200ms between every test to prevent rate-limiting - static { // Log trace-level System.setProperty("org.slf4j.simpleLogger.log.net.jacobpeterson", "trace"); } + private static final Logger LOGGER = LoggerFactory.getLogger(AlpacaAPITest.class); + private static final int RATE_LIMIT_MILLIS = 200; // Wait 200ms between every test to prevent rate-limiting + private static AlpacaAPI alpacaAPI; private static boolean marketOpen; private static AccountConfiguration accountConfiguration; @@ -83,7 +91,7 @@ public void afterEach() { */ @Test @org.junit.jupiter.api.Order(1) - public void test_ClockEndpoint_get() throws AlpacaClientException { + public void testClockEndpointGet() throws AlpacaClientException { Clock clock = alpacaAPI.clock().get(); assertNotNull(clock); @@ -110,7 +118,7 @@ public void test_ClockEndpoint_get() throws AlpacaClientException { */ @Test @SuppressWarnings("ResultOfMethodCallIgnored") - public void test_AccountEndpoint_get() throws AlpacaClientException, NumberFormatException { + public void testAccountEndpointGet() throws AlpacaClientException, NumberFormatException { Account account = alpacaAPI.account().get(); assertNotNull(account); @@ -148,14 +156,13 @@ public void test_AccountEndpoint_get() throws AlpacaClientException, NumberForma } /** - * Tests @{@link AccountActivitiesEndpoint#get(ZonedDateTime, - * ZonedDateTime, ZonedDateTime, SortDirection, Integer, String, ActivityType...)} one {@link AccountActivity} - * exists until now. + * Tests @{@link AccountActivitiesEndpoint#get(ZonedDateTime, ZonedDateTime, ZonedDateTime, SortDirection, Integer, + * String, ActivityType...)} one {@link AccountActivity} exists until now. * * @throws AlpacaClientException thrown for {@link AlpacaClientException}s */ @Test - public void test_AccountActivitiesEndpoint_get_One_Activity_Exists_Until_Now() throws AlpacaClientException { + public void testAccountActivitiesEndpointGetOneActivityExistsUntilNow() throws AlpacaClientException { List accountActivities = alpacaAPI.accountActivities().get( null, ZonedDateTime.now(), @@ -203,7 +210,7 @@ public void test_AccountActivitiesEndpoint_get_One_Activity_Exists_Until_Now() t */ @Test @org.junit.jupiter.api.Order(1) - public void test_AccountConfigurationEndpoint_get() throws AlpacaClientException { + public void testAccountConfigurationEndpointGet() throws AlpacaClientException { AccountConfiguration accountConfiguration = alpacaAPI.accountConfiguration().get(); assertNotNull(accountConfiguration); @@ -224,7 +231,7 @@ public void test_AccountConfigurationEndpoint_get() throws AlpacaClientException */ @Test @org.junit.jupiter.api.Order(2) - public void test_AccountConfigurationEndpoint_set() throws AlpacaClientException { + public void testAccountConfigurationEndpointSet() throws AlpacaClientException { if (accountConfiguration == null) { AccountConfiguration newAccountConfiguration = new AccountConfiguration( DTBPCheck.BOTH, @@ -240,12 +247,12 @@ public void test_AccountConfigurationEndpoint_set() throws AlpacaClientException /** * Test {@link OrdersEndpoint#get(CurrentOrderStatus, Integer, ZonedDateTime, ZonedDateTime, SortDirection, Boolean, - * List)} one {@link Order} exists until now. + * Collection)} one {@link Order} exists until now. * * @throws AlpacaClientException thrown for {@link AlpacaClientException}s */ @Test - public void test_OrdersEndpoint_get_One_Order_Exists_Until_now() throws AlpacaClientException { + public void testOrdersEndpointGetOneOrderExistsUntilNow() throws AlpacaClientException { List orders = alpacaAPI.orders().get( CurrentOrderStatus.ALL, 1, From 95cc9352c032cdbc089c151da43557fa1bcabfca Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Thu, 11 Nov 2021 00:08:48 -0700 Subject: [PATCH 16/84] Add more notes about AlpacaAPI constructors in README, fix javadocs --- README.md | 9 ++++-- .../net/jacobpeterson/alpaca/AlpacaAPI.java | 32 +++++++++---------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index f571e41e..a0dce667 100644 --- a/README.md +++ b/README.md @@ -60,10 +60,15 @@ Example usage: // This constructor uses the 'alpaca.properties' file on the classpath for configuration AlpacaAPI alpacaAPI = new AlpacaAPI(); -// This constructor passes in a 'keyID' and 'secret' String +// This constructor passes in a 'keyID' and 'secretKey' and uses the endpoint API type and data API +// type defined in the 'alpaca.properties' file (which default to 'paper' and 'iex' respectively) String keyID = ""; String secretKey = ""; -AlpacaAPI alpacaAPI = new AlpacaAPI(keyID, secret); +AlpacaAPI alpacaAPI = new AlpacaAPI(keyID, secretKey); + +// This constructor passes in a 'keyID' and 'secretKey' and uses the passed in endpoint API type +// and data API type (which are 'LIVE' and 'SIP' respectively in this example) +AlpacaAPI alpacaAPI = new AlpacaAPI(keyID, secretKey, EndpointAPIType.LIVE, DataAPIType.SIP); // This constructor is for OAuth tokens String oAuthToken = ""; diff --git a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java index 616e53e4..b874d794 100644 --- a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java +++ b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java @@ -75,11 +75,11 @@ public AlpacaAPI() { /** * Instantiates a new {@link AlpacaAPI}. * - * @param keyID the key ID - * @param secret the secret + * @param keyID the key ID + * @param secretKey the secret key */ - public AlpacaAPI(String keyID, String secret) { - this(null, keyID, secret, null, + public AlpacaAPI(String keyID, String secretKey) { + this(null, keyID, secretKey, null, AlpacaProperties.ENDPOINT_API_TYPE, AlpacaProperties.DATA_API_TYPE); } @@ -88,12 +88,12 @@ public AlpacaAPI(String keyID, String secret) { * Instantiates a new {@link AlpacaAPI}. * * @param keyID the key ID - * @param secret the secret + * @param secretKey the secret key * @param endpointAPIType the {@link EndpointAPIType} * @param dataAPIType the {@link DataAPIType} */ - public AlpacaAPI(String keyID, String secret, EndpointAPIType endpointAPIType, DataAPIType dataAPIType) { - this(null, keyID, secret, null, endpointAPIType, dataAPIType); + public AlpacaAPI(String keyID, String secretKey, EndpointAPIType endpointAPIType, DataAPIType dataAPIType) { + this(null, keyID, secretKey, null, endpointAPIType, dataAPIType); } /** @@ -155,11 +155,9 @@ public AlpacaAPI(OkHttpClient okHttpClient, String keyID, String secretKey, Stri brokerClient = new AlpacaClient(okHttpClient, keyID, secretKey, brokerHostSubdomain, VERSION_2_PATH_SEGMENT); dataClient = new AlpacaClient(okHttpClient, keyID, secretKey, "data", VERSION_2_PATH_SEGMENT); - marketDataWebsocket = new MarketDataWebsocket(okHttpClient, dataAPIType, keyID, secretKey); } else { brokerClient = new AlpacaClient(okHttpClient, oAuthToken, brokerHostSubdomain, VERSION_2_PATH_SEGMENT); dataClient = null; - marketDataWebsocket = null; } accountEndpoint = new AccountEndpoint(brokerClient); @@ -174,10 +172,12 @@ public AlpacaAPI(OkHttpClient okHttpClient, String keyID, String secretKey, Stri accountActivitiesEndpoint = new AccountActivitiesEndpoint(brokerClient); portfolioHistoryEndpoint = new PortfolioHistoryEndpoint(brokerClient); streamingWebsocket = new StreamingWebsocket(okHttpClient, brokerHostSubdomain, keyID, secretKey, oAuthToken); + marketDataWebsocket = dataClient == null ? null : + new MarketDataWebsocket(okHttpClient, dataAPIType, keyID, secretKey); } /** - * @return {@link AccountEndpoint} + * @return the {@link AccountEndpoint} */ public AccountEndpoint account() { return accountEndpoint; @@ -191,42 +191,42 @@ public MarketDataEndpoint marketData() { } /** - * @return {@link OrdersEndpoint} + * @return the {@link OrdersEndpoint} */ public OrdersEndpoint orders() { return ordersEndpoint; } /** - * @return {@link PositionsEndpoint} + * @return the {@link PositionsEndpoint} */ public PositionsEndpoint positions() { return positionsEndpoint; } /** - * @return {@link AssetsEndpoint} + * @return the {@link AssetsEndpoint} */ public AssetsEndpoint assets() { return assetsEndpoint; } /** - * @return {@link WatchlistEndpoint} + * @return the {@link WatchlistEndpoint} */ public WatchlistEndpoint watchlist() { return watchlistEndpoint; } /** - * @return {@link CalendarEndpoint} + * @return the {@link CalendarEndpoint} */ public CalendarEndpoint calendar() { return calendarEndpoint; } /** - * @return {@link ClockEndpoint} + * @return the {@link ClockEndpoint} */ public ClockEndpoint clock() { return clockEndpoint; From a55256c88fda84cdec0fd22f275d2bfaac2d17ca Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Fri, 19 Nov 2021 09:19:34 -0700 Subject: [PATCH 17/84] Add SSP and SPLIT ActivityType --- .../account_activities/enums/activity_type.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/schema_json/endpoint/account_activities/enums/activity_type.json b/schema_json/endpoint/account_activities/enums/activity_type.json index 03c44af1..9fae432b 100644 --- a/schema_json/endpoint/account_activities/enums/activity_type.json +++ b/schema_json/endpoint/account_activities/enums/activity_type.json @@ -35,7 +35,9 @@ "FEE", "REORG", "SC", - "SSO" + "SSO", + "SSP", + "SPLIT" ], "javaEnums": [ { @@ -139,6 +141,12 @@ }, { "description": "Stock spinoff." + }, + { + "description": "Stock split." + }, + { + "description": "Stock split." } ] } From 1bde139826f8dc4efcddd0f1d7f2b758ad53b284 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Fri, 19 Nov 2021 09:20:48 -0700 Subject: [PATCH 18/84] Update to 8.3.2 --- README.md | 4 ++-- build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a0dce667..d1443c3d 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ If you are using Gradle as your build tool, add the following dependency to your ``` dependencies { - implementation group: 'net.jacobpeterson', name: 'alpaca-java', version: '8.3.1' + implementation group: 'net.jacobpeterson', name: 'alpaca-java', version: '8.3.2' } ``` @@ -27,7 +27,7 @@ If you are using Maven as your build tool, add the following dependency to your net.jacobpeterson alpaca-java - 8.3.1 + 8.3.2 compile ``` diff --git a/build.gradle b/build.gradle index d5566da8..da880edf 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,7 @@ tasks.getByName("generateJsonSchema2Pojo").enabled(false) // Disable default 'js archivesBaseName = 'alpaca-java' group = 'net.jacobpeterson' -version = '8.3.1-SNAPSHOT' +version = '8.3.2-SNAPSHOT' repositories { mavenCentral() From 6a45b2bcd2fbdba6c04135a905cf2559152b5e95 Mon Sep 17 00:00:00 2001 From: kurru Date: Sat, 4 Dec 2021 16:11:11 -0800 Subject: [PATCH 19/84] define new account status enum --- schema_json/endpoint/account/enums/account_status.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/schema_json/endpoint/account/enums/account_status.json b/schema_json/endpoint/account/enums/account_status.json index 65963f0d..61f7a081 100644 --- a/schema_json/endpoint/account/enums/account_status.json +++ b/schema_json/endpoint/account/enums/account_status.json @@ -9,7 +9,8 @@ "APPROVAL_PENDING", "ACTIVE", "REJECTED", - "DISABLED" + "DISABLED", + "ACTION_REQUIRED" ], "javaEnums": [ { @@ -35,6 +36,9 @@ }, { "description": "The account has been disabled." + }, + { + "description": "The account requires manual attention." } ] } From 6ea0ff40050666fc15348717f020047c94a346ce Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Sun, 5 Dec 2021 17:01:03 -0700 Subject: [PATCH 20/84] Update to 8.3.3 --- README.md | 4 ++-- build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d1443c3d..2be165a6 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ If you are using Gradle as your build tool, add the following dependency to your ``` dependencies { - implementation group: 'net.jacobpeterson', name: 'alpaca-java', version: '8.3.2' + implementation group: 'net.jacobpeterson', name: 'alpaca-java', version: '8.3.3' } ``` @@ -27,7 +27,7 @@ If you are using Maven as your build tool, add the following dependency to your net.jacobpeterson alpaca-java - 8.3.2 + 8.3.3 compile ``` diff --git a/build.gradle b/build.gradle index da880edf..88519ffb 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,7 @@ tasks.getByName("generateJsonSchema2Pojo").enabled(false) // Disable default 'js archivesBaseName = 'alpaca-java' group = 'net.jacobpeterson' -version = '8.3.2-SNAPSHOT' +version = '8.3.3-SNAPSHOT' repositories { mavenCentral() From 6f8f037a349ba47638a4a2672e9d946b73df45fd Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Sun, 19 Dec 2021 21:49:15 -0700 Subject: [PATCH 21/84] Add more descriptive field names in JSON schemas, fix javadoc formatting --- README.md | 2 +- .../non_trade_activity.json | 1 + .../account_activities/trade_activity.json | 3 +++ .../market_data/historical/bar/bar.json | 8 ++++++++ .../market_data/historical/quote/quote.json | 8 ++++++++ .../market_data/historical/trade/trade.json | 7 +++++++ .../realtime/quote/quote_message.json | 4 ++-- schema_json/endpoint/order/order.json | 8 ++++---- .../portfolio_history_response.json | 1 + schema_json/endpoint/position/position.json | 6 ++++++ .../streaming/trade/trade_update.json | 1 + .../alpaca/properties/AlpacaProperties.java | 20 ++++++++++++++----- .../endpoint/PortfolioHistoryEndpoint.java | 4 ++-- .../util/doc/Documentation2JSONSchema.java | 3 ++- .../alpaca/util/format/FormatUtil.java | 4 +++- .../alpaca/util/gson/GsonUtil.java | 7 ++++++- .../gson/{ => adapters}/LocalDateAdapter.java | 2 +- .../gson/{ => adapters}/LocalTimeAdapter.java | 2 +- .../{ => adapters}/ZonedDateTimeAdapter.java | 2 +- .../alpaca/util/okhttp/JSONBodyBuilder.java | 4 +++- .../alpaca/websocket/AlpacaWebsocket.java | 4 +++- .../alpaca/test/live/AlpacaAPITest.java | 12 +++++------ 22 files changed, 85 insertions(+), 28 deletions(-) rename src/main/java/net/jacobpeterson/alpaca/util/gson/{ => adapters}/LocalDateAdapter.java (95%) rename src/main/java/net/jacobpeterson/alpaca/util/gson/{ => adapters}/LocalTimeAdapter.java (95%) rename src/main/java/net/jacobpeterson/alpaca/util/gson/{ => adapters}/ZonedDateTimeAdapter.java (95%) diff --git a/README.md b/README.md index 2be165a6..de6fdc01 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ If you are using Maven as your build tool, add the following dependency to your Note that you don't have to use the Maven Central artifacts and instead can just install a clone of this project to your local Maven repository as shown in the [Building](#building) section. # Configuration -Creating an `alpaca.properties` file on the classpath with the following format allows you to easily load properties using the `AlpacaAPI` default constructor: +Creating an `alpaca.properties` file on the classpath (e.g. in `src/main/resources/alpaca.properties`) with the following format allows you to easily load properties using the `AlpacaAPI` default constructor: ``` key_id = secret_key = diff --git a/schema_json/endpoint/account_activities/non_trade_activity.json b/schema_json/endpoint/account_activities/non_trade_activity.json index c66a15af..81d8c0fa 100644 --- a/schema_json/endpoint/account_activities/non_trade_activity.json +++ b/schema_json/endpoint/account_activities/non_trade_activity.json @@ -19,6 +19,7 @@ }, "qty": { "existingJavaType": "java.lang.String", + "javaName": "quantity", "title": "For dividend activities, the number of shares that contributed to the payment. Not present for other activity types." }, "per_share_amount": { diff --git a/schema_json/endpoint/account_activities/trade_activity.json b/schema_json/endpoint/account_activities/trade_activity.json index 0c6dd5f4..1697ad1c 100644 --- a/schema_json/endpoint/account_activities/trade_activity.json +++ b/schema_json/endpoint/account_activities/trade_activity.json @@ -7,10 +7,12 @@ "properties": { "cum_qty": { "existingJavaType": "java.lang.String", + "javaName": "cumulativeQuantity", "title": "The cumulative quantity of shares involved in the execution." }, "leaves_qty": { "existingJavaType": "java.lang.String", + "javaName": "remainingQuantity", "title": "For partially_filled orders, the quantity of shares that are left to be filled." }, "price": { @@ -19,6 +21,7 @@ }, "qty": { "existingJavaType": "java.lang.String", + "javaName": "quantity", "title": "The number of shares involved in the trade execution." }, "side": { diff --git a/schema_json/endpoint/market_data/historical/bar/bar.json b/schema_json/endpoint/market_data/historical/bar/bar.json index 6be1fe66..55423fde 100644 --- a/schema_json/endpoint/market_data/historical/bar/bar.json +++ b/schema_json/endpoint/market_data/historical/bar/bar.json @@ -4,34 +4,42 @@ "properties": { "t": { "existingJavaType": "java.time.ZonedDateTime", + "javaName": "timestamp", "title": "Timestamp with nanosecond precision." }, "o": { "existingJavaType": "java.lang.Double", + "javaName": "open", "title": "Open price." }, "h": { "existingJavaType": "java.lang.Double", + "javaName": "high", "title": "High price." }, "l": { "existingJavaType": "java.lang.Double", + "javaName": "low", "title": "Low price." }, "c": { "existingJavaType": "java.lang.Double", + "javaName": "close", "title": "Close price." }, "v": { "existingJavaType": "java.lang.Long", + "javaName": "volume", "title": "Volume." }, "n": { "existingJavaType": "java.lang.Long", + "javaName": "tradeCount", "title": "Trade count." }, "vw": { "existingJavaType": "java.lang.Double", + "javaName": "vwap", "title": "VWAP (Volume Weighted Average Price)." } } diff --git a/schema_json/endpoint/market_data/historical/quote/quote.json b/schema_json/endpoint/market_data/historical/quote/quote.json index 7a238f6e..059c9746 100644 --- a/schema_json/endpoint/market_data/historical/quote/quote.json +++ b/schema_json/endpoint/market_data/historical/quote/quote.json @@ -4,34 +4,42 @@ "properties": { "t": { "existingJavaType": "java.time.ZonedDateTime", + "javaName": "timestamp", "title": "Timestamp with nanosecond precision." }, "ax": { "existingJavaType": "java.lang.String", + "javaName": "askExchange", "title": "Ask exchange." }, "ap": { "existingJavaType": "java.lang.Double", + "javaName": "askPrice", "title": "Ask price." }, "as": { "existingJavaType": "java.lang.Integer", + "javaName": "askSize", "title": "Ask size." }, "bx": { "existingJavaType": "java.lang.String", + "javaName": "bidExchange", "title": "Bid exchange." }, "bp": { "existingJavaType": "java.lang.Double", + "javaName": "bidPrice", "title": "Bid price." }, "bs": { "existingJavaType": "java.lang.Integer", + "javaName": "bidSize", "title": "Bid size." }, "c": { "existingJavaType": "java.util.ArrayList", + "javaName": "conditions", "title": "The {@link java.util.ArrayList} of quote conditions." } } diff --git a/schema_json/endpoint/market_data/historical/trade/trade.json b/schema_json/endpoint/market_data/historical/trade/trade.json index c9303e7b..f7ef48ca 100644 --- a/schema_json/endpoint/market_data/historical/trade/trade.json +++ b/schema_json/endpoint/market_data/historical/trade/trade.json @@ -4,30 +4,37 @@ "properties": { "t": { "existingJavaType": "java.time.ZonedDateTime", + "javaName": "timestamp", "title": "Timestamp with nanosecond precision." }, "x": { "existingJavaType": "java.lang.String", + "javaName": "exchange", "title": "Exchange where the trade happened." }, "p": { "existingJavaType": "java.lang.Double", + "javaName": "price", "title": "Trade price." }, "s": { "existingJavaType": "java.lang.Integer", + "javaName": "size", "title": "Trade size." }, "c": { "existingJavaType": "java.util.ArrayList", + "javaName": "conditions", "title": "The {@link java.util.ArrayList} of trade conditions." }, "i": { "existingJavaType": "java.lang.Long", + "javaName": "tradeID", "title": "Trade ID." }, "z": { "existingJavaType": "java.lang.String", + "javaName": "tape", "title": "Tape." } } diff --git a/schema_json/endpoint/market_data/realtime/quote/quote_message.json b/schema_json/endpoint/market_data/realtime/quote/quote_message.json index a8ff47b3..f1f9606d 100644 --- a/schema_json/endpoint/market_data/realtime/quote/quote_message.json +++ b/schema_json/endpoint/market_data/realtime/quote/quote_message.json @@ -7,7 +7,7 @@ "properties": { "ax": { "existingJavaType": "java.lang.String", - "javaName": "askExchangeCode", + "javaName": "askExchange", "title": "Ask exchange code." }, "ap": { @@ -22,7 +22,7 @@ }, "bx": { "existingJavaType": "java.lang.String", - "javaName": "bidExchangeCode", + "javaName": "bidExchange", "title": "Bid exchange code." }, "bp": { diff --git a/schema_json/endpoint/order/order.json b/schema_json/endpoint/order/order.json index 4af8dde8..c0af5330 100644 --- a/schema_json/endpoint/order/order.json +++ b/schema_json/endpoint/order/order.json @@ -68,24 +68,23 @@ }, "qty": { "existingJavaType": "java.lang.String", + "javaName": "quantity", "title": "Ordered quantity. If entered, notional will be null. Can take up to 9 decimal points." }, "filled_qty": { "existingJavaType": "java.lang.String", + "javaName": "filledQuantity", "title": "Filled quantity." }, "filled_avg_price": { "existingJavaType": "java.lang.String", + "javaName": "averageFillPrice", "title": "Filled average price." }, "order_class": { "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.order.enums.OrderClass", "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.order.enums.OrderClass}. For details of non-simple order classes, please see Bracket Order Overview." }, - "order_type": { - "existingJavaType": "java.lang.String", - "title": "(Deprecated with just type field below.)." - }, "type": { "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.order.enums.OrderType", "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.order.enums.OrderType}." @@ -128,6 +127,7 @@ }, "hwm": { "existingJavaType": "java.lang.String", + "javaName": "highWaterMark", "title": "High Water Mark - The highest (lowest) market price seen since the trailing stop order was submitted." } } diff --git a/schema_json/endpoint/portfolio_history/portfolio_history_response.json b/schema_json/endpoint/portfolio_history/portfolio_history_response.json index d43235f7..d3ca8177 100644 --- a/schema_json/endpoint/portfolio_history/portfolio_history_response.json +++ b/schema_json/endpoint/portfolio_history/portfolio_history_response.json @@ -16,6 +16,7 @@ }, "profit_loss_pct": { "existingJavaType": "java.util.ArrayList", + "javaName": "profitLossPercent", "title": "Profit/loss in percentage from the base value." }, "base_value": { diff --git a/schema_json/endpoint/position/position.json b/schema_json/endpoint/position/position.json index bee7e824..2f1eb603 100644 --- a/schema_json/endpoint/position/position.json +++ b/schema_json/endpoint/position/position.json @@ -20,10 +20,12 @@ }, "avg_entry_price": { "existingJavaType": "java.lang.String", + "javaName": "averageEntryPrice", "title": "Average entry price of the position." }, "qty": { "existingJavaType": "java.lang.String", + "javaName": "quantity", "title": "The number of shares." }, "side": { @@ -40,18 +42,22 @@ }, "unrealized_pl": { "existingJavaType": "java.lang.String", + "javaName": "unrealizedProfitLoss", "title": "Unrealized profit/loss in dollars." }, "unrealized_plpc": { "existingJavaType": "java.lang.String", + "javaName": "unrealizedProfitLossPercent", "title": "Unrealized profit/loss percent (by a factor of 1)." }, "unrealized_intraday_pl": { "existingJavaType": "java.lang.String", + "javaName": "unrealizedIntradayProfitLoss", "title": "Unrealized profit/loss in dollars for the day." }, "unrealized_intraday_plpc": { "existingJavaType": "java.lang.String", + "javaName": "unrealizedIntradayProfitLossPercent", "title": "Unrealized profit/loss percent (by a factor of 1)." }, "current_price": { diff --git a/schema_json/endpoint/streaming/trade/trade_update.json b/schema_json/endpoint/streaming/trade/trade_update.json index 46188863..339ea01c 100644 --- a/schema_json/endpoint/streaming/trade/trade_update.json +++ b/schema_json/endpoint/streaming/trade/trade_update.json @@ -16,6 +16,7 @@ }, "position_qty": { "existingJavaType": "java.lang.String", + "javaName": "positionQuantity", "title": "The position quantity." }, "order": { diff --git a/src/main/java/net/jacobpeterson/alpaca/properties/AlpacaProperties.java b/src/main/java/net/jacobpeterson/alpaca/properties/AlpacaProperties.java index 2b8d0b79..0c121157 100644 --- a/src/main/java/net/jacobpeterson/alpaca/properties/AlpacaProperties.java +++ b/src/main/java/net/jacobpeterson/alpaca/properties/AlpacaProperties.java @@ -15,31 +15,41 @@ public class AlpacaProperties { private static final String ALPACA_DEFAULT_PROPERTIES_FILE = "alpaca.default.properties"; private static final String KEY_ID_KEY = "key_id"; - /** The value of {@link #KEY_ID_KEY} in {@link #ALPACA_PROPERTIES_FILE}. */ + /** + * The value of {@link #KEY_ID_KEY} in {@link #ALPACA_PROPERTIES_FILE}. + */ public static final String KEY_ID = getProperty( ALPACA_PROPERTIES_FILE, ALPACA_DEFAULT_PROPERTIES_FILE, KEY_ID_KEY); private static final String SECRET_KEY_KEY = "secret_key"; - /** The value of {@link #SECRET_KEY_KEY} in {@link #ALPACA_PROPERTIES_FILE}. */ + /** + * The value of {@link #SECRET_KEY_KEY} in {@link #ALPACA_PROPERTIES_FILE}. + */ public static final String SECRET_KEY = getProperty( ALPACA_PROPERTIES_FILE, ALPACA_DEFAULT_PROPERTIES_FILE, SECRET_KEY_KEY); private static final String ENDPOINT_API_TYPE_KEY = "endpoint_api_type"; - /** The value of {@link #ENDPOINT_API_TYPE_KEY} in {@link #ALPACA_PROPERTIES_FILE}. */ + /** + * The value of {@link #ENDPOINT_API_TYPE_KEY} in {@link #ALPACA_PROPERTIES_FILE}. + */ public static final EndpointAPIType ENDPOINT_API_TYPE = EndpointAPIType.fromValue(getProperty( ALPACA_PROPERTIES_FILE, ALPACA_DEFAULT_PROPERTIES_FILE, ENDPOINT_API_TYPE_KEY)); private static final String DATA_API_TYPE_KEY = "data_api_type"; - /** The value of {@link #DATA_API_TYPE_KEY} in {@link #ALPACA_PROPERTIES_FILE}. */ + /** + * The value of {@link #DATA_API_TYPE_KEY} in {@link #ALPACA_PROPERTIES_FILE}. + */ public static final DataAPIType DATA_API_TYPE = DataAPIType.fromValue(getProperty( ALPACA_PROPERTIES_FILE, ALPACA_DEFAULT_PROPERTIES_FILE, DATA_API_TYPE_KEY)); private static final String USER_AGENT_KEY = "user_agent"; - /** The value of {@link #USER_AGENT_KEY} in {@link #ALPACA_PROPERTIES_FILE}. */ + /** + * The value of {@link #USER_AGENT_KEY} in {@link #ALPACA_PROPERTIES_FILE}. + */ public static final String USER_AGENT = getProperty( ALPACA_PROPERTIES_FILE, ALPACA_DEFAULT_PROPERTIES_FILE, USER_AGENT_KEY); diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/PortfolioHistoryEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/PortfolioHistoryEndpoint.java index 9bab136d..1d01c19d 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/PortfolioHistoryEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/PortfolioHistoryEndpoint.java @@ -81,7 +81,7 @@ public PortfolioHistory get(Integer periodLength, PortfolioPeriodUnit periodUnit // Check if any response arrays differ in size checkState(response.getTimestamp().size() == response.getEquity().size() && response.getEquity().size() == response.getProfitLoss().size() && - response.getProfitLoss().size() == response.getProfitLossPct().size(), + response.getProfitLoss().size() == response.getProfitLossPercent().size(), "Response arrays should not differ in size!"); // Add all data points into one POJO @@ -91,7 +91,7 @@ public PortfolioHistory get(Integer periodLength, PortfolioPeriodUnit periodUnit Instant.ofEpochSecond(response.getTimestamp().get(index)).atZone(FormatUtil.NEW_YORK_ZONED_ID), response.getEquity().get(index), response.getProfitLoss().get(index), - response.getProfitLossPct().get(index))); + response.getProfitLossPercent().get(index))); } return new PortfolioHistory( diff --git a/src/main/java/net/jacobpeterson/alpaca/util/doc/Documentation2JSONSchema.java b/src/main/java/net/jacobpeterson/alpaca/util/doc/Documentation2JSONSchema.java index 29fbf597..ba0e2208 100644 --- a/src/main/java/net/jacobpeterson/alpaca/util/doc/Documentation2JSONSchema.java +++ b/src/main/java/net/jacobpeterson/alpaca/util/doc/Documentation2JSONSchema.java @@ -16,7 +16,8 @@ import java.io.IOException; /** - * The type {@link Documentation2JSONSchema}. + * The {@link Documentation2JSONSchema} is used strictly to expedite the process of creating JSON schemas for POJO + * generation in this library using Alpaca's website documentation. */ public class Documentation2JSONSchema { diff --git a/src/main/java/net/jacobpeterson/alpaca/util/format/FormatUtil.java b/src/main/java/net/jacobpeterson/alpaca/util/format/FormatUtil.java index b5a53a27..e797c294 100644 --- a/src/main/java/net/jacobpeterson/alpaca/util/format/FormatUtil.java +++ b/src/main/java/net/jacobpeterson/alpaca/util/format/FormatUtil.java @@ -14,7 +14,9 @@ */ public class FormatUtil { - /** The {@link ZoneId} for America/New_York. */ + /** + * The {@link ZoneId} for America/New_York. + */ public static final ZoneId NEW_YORK_ZONED_ID = ZoneId.of("America/New_York"); // Alpaca uses the following rounding mechanics with respect to buy orders: (1) rounded down to two decimal diff --git a/src/main/java/net/jacobpeterson/alpaca/util/gson/GsonUtil.java b/src/main/java/net/jacobpeterson/alpaca/util/gson/GsonUtil.java index 7b94a344..dc86e2c1 100644 --- a/src/main/java/net/jacobpeterson/alpaca/util/gson/GsonUtil.java +++ b/src/main/java/net/jacobpeterson/alpaca/util/gson/GsonUtil.java @@ -2,6 +2,9 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import net.jacobpeterson.alpaca.util.gson.adapters.LocalDateAdapter; +import net.jacobpeterson.alpaca.util.gson.adapters.LocalTimeAdapter; +import net.jacobpeterson.alpaca.util.gson.adapters.ZonedDateTimeAdapter; import java.time.LocalDate; import java.time.LocalTime; @@ -12,7 +15,9 @@ */ public class GsonUtil { - /** A {@link Gson} instance with registered date/time adapters. */ + /** + * A {@link Gson} instance with registered date/time adapters. + */ public static final Gson GSON = new GsonBuilder() .registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeAdapter()) .registerTypeAdapter(LocalDate.class, new LocalDateAdapter()) diff --git a/src/main/java/net/jacobpeterson/alpaca/util/gson/LocalDateAdapter.java b/src/main/java/net/jacobpeterson/alpaca/util/gson/adapters/LocalDateAdapter.java similarity index 95% rename from src/main/java/net/jacobpeterson/alpaca/util/gson/LocalDateAdapter.java rename to src/main/java/net/jacobpeterson/alpaca/util/gson/adapters/LocalDateAdapter.java index d1dc5956..a9475c17 100644 --- a/src/main/java/net/jacobpeterson/alpaca/util/gson/LocalDateAdapter.java +++ b/src/main/java/net/jacobpeterson/alpaca/util/gson/adapters/LocalDateAdapter.java @@ -1,4 +1,4 @@ -package net.jacobpeterson.alpaca.util.gson; +package net.jacobpeterson.alpaca.util.gson.adapters; import com.google.gson.Gson; import com.google.gson.JsonDeserializationContext; diff --git a/src/main/java/net/jacobpeterson/alpaca/util/gson/LocalTimeAdapter.java b/src/main/java/net/jacobpeterson/alpaca/util/gson/adapters/LocalTimeAdapter.java similarity index 95% rename from src/main/java/net/jacobpeterson/alpaca/util/gson/LocalTimeAdapter.java rename to src/main/java/net/jacobpeterson/alpaca/util/gson/adapters/LocalTimeAdapter.java index 960ccda2..9d6bb735 100644 --- a/src/main/java/net/jacobpeterson/alpaca/util/gson/LocalTimeAdapter.java +++ b/src/main/java/net/jacobpeterson/alpaca/util/gson/adapters/LocalTimeAdapter.java @@ -1,4 +1,4 @@ -package net.jacobpeterson.alpaca.util.gson; +package net.jacobpeterson.alpaca.util.gson.adapters; import com.google.gson.Gson; import com.google.gson.JsonDeserializationContext; diff --git a/src/main/java/net/jacobpeterson/alpaca/util/gson/ZonedDateTimeAdapter.java b/src/main/java/net/jacobpeterson/alpaca/util/gson/adapters/ZonedDateTimeAdapter.java similarity index 95% rename from src/main/java/net/jacobpeterson/alpaca/util/gson/ZonedDateTimeAdapter.java rename to src/main/java/net/jacobpeterson/alpaca/util/gson/adapters/ZonedDateTimeAdapter.java index 7f1cf2a7..e10f1be1 100644 --- a/src/main/java/net/jacobpeterson/alpaca/util/gson/ZonedDateTimeAdapter.java +++ b/src/main/java/net/jacobpeterson/alpaca/util/gson/adapters/ZonedDateTimeAdapter.java @@ -1,4 +1,4 @@ -package net.jacobpeterson.alpaca.util.gson; +package net.jacobpeterson.alpaca.util.gson.adapters; import com.google.gson.Gson; import com.google.gson.JsonDeserializationContext; diff --git a/src/main/java/net/jacobpeterson/alpaca/util/okhttp/JSONBodyBuilder.java b/src/main/java/net/jacobpeterson/alpaca/util/okhttp/JSONBodyBuilder.java index c6f54498..11944345 100644 --- a/src/main/java/net/jacobpeterson/alpaca/util/okhttp/JSONBodyBuilder.java +++ b/src/main/java/net/jacobpeterson/alpaca/util/okhttp/JSONBodyBuilder.java @@ -10,7 +10,9 @@ */ public class JSONBodyBuilder { - /** The UTF-8 JSON {@link MediaType}. */ + /** + * The UTF-8 JSON {@link MediaType}. + */ public static final MediaType JSON_MEDIA_TYPE = MediaType.get("application/json; charset=UTF-8"); private String bodyJSON; diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java index 331d89da..232d23f2 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java @@ -42,7 +42,9 @@ public abstract class AlpacaWebsocket Date: Mon, 20 Dec 2021 02:03:01 -0700 Subject: [PATCH 22/84] Refactor, add CryptoMarketDataEndpoint --- README.md | 43 ++- schema_json/endpoint/asset/asset.json | 5 +- .../endpoint/asset/enums/asset_class.json | 8 + .../bar => common}/enums/bar_time_period.json | 2 +- .../crypto/historical/bar/bar.json | 51 +++ .../crypto/historical/bar/bars_response.json | 18 ++ .../crypto/historical/enums/exchange.json | 20 ++ .../quote/latest_quote_response.json | 14 + .../crypto/historical/quote/quote.json | 36 +++ .../historical/quote/quotes_response.json | 18 ++ .../historical/trade/enums/taker_side.json | 16 + .../trade/latest_trade_response.json | 14 + .../crypto/historical/trade/trade.json | 36 +++ .../historical/trade/trades_response.json | 18 ++ .../crypto/historical/xbbo/xbbo.json | 41 +++ .../crypto/historical/xbbo/xbbo_response.json | 14 + .../{ => stock}/historical/bar/bar.json | 0 .../historical/bar/bars_response.json | 4 +- .../historical/bar/enums/bar_adjustment.json | 0 .../quote/latest_quote_response.json | 4 +- .../{ => stock}/historical/quote/quote.json | 0 .../historical/quote/quotes_response.json | 4 +- .../historical/snapshot/snapshot.json | 20 +- .../trade/latest_trade_response.json | 4 +- .../{ => stock}/historical/trade/trade.json | 0 .../historical/trade/trades_response.json | 4 +- .../{ => stock}/realtime/bar/bar_message.json | 2 +- .../realtime/control/error_message.json | 2 +- .../control/subscriptions_message.json | 2 +- .../realtime/control/success_message.json | 2 +- .../enums/market_data_message_type.json | 0 .../realtime/market_data_message.json | 4 +- .../realtime/quote/quote_message.json | 2 +- .../{ => stock}/realtime/symbol_message.json | 2 +- .../realtime/trade/trade_message.json | 2 +- schema_json/endpoint/position/position.json | 2 +- .../net/jacobpeterson/alpaca/AlpacaAPI.java | 57 ++-- .../alpaca/rest/AlpacaClient.java | 10 - .../alpaca/rest/AlpacaClientException.java | 20 -- .../endpoint/AccountActivitiesEndpoint.java | 2 +- .../alpaca/rest/endpoint/AssetsEndpoint.java | 7 +- .../alpaca/rest/endpoint/OrdersEndpoint.java | 4 +- .../marketdata/CryptoMarketDataEndpoint.java | 296 ++++++++++++++++++ .../StockMarketDataEndpoint.java} | 44 +-- .../alpaca/websocket/AlpacaWebsocket.java | 20 -- .../marketdata/MarketDataListener.java | 4 +- .../marketdata/MarketDataWebsocket.java | 16 +- .../MarketDataWebsocketInterface.java | 10 +- 48 files changed, 744 insertions(+), 160 deletions(-) create mode 100644 schema_json/endpoint/asset/enums/asset_class.json rename schema_json/endpoint/market_data/{historical/bar => common}/enums/bar_time_period.json (75%) create mode 100644 schema_json/endpoint/market_data/crypto/historical/bar/bar.json create mode 100644 schema_json/endpoint/market_data/crypto/historical/bar/bars_response.json create mode 100644 schema_json/endpoint/market_data/crypto/historical/enums/exchange.json create mode 100644 schema_json/endpoint/market_data/crypto/historical/quote/latest_quote_response.json create mode 100644 schema_json/endpoint/market_data/crypto/historical/quote/quote.json create mode 100644 schema_json/endpoint/market_data/crypto/historical/quote/quotes_response.json create mode 100644 schema_json/endpoint/market_data/crypto/historical/trade/enums/taker_side.json create mode 100644 schema_json/endpoint/market_data/crypto/historical/trade/latest_trade_response.json create mode 100644 schema_json/endpoint/market_data/crypto/historical/trade/trade.json create mode 100644 schema_json/endpoint/market_data/crypto/historical/trade/trades_response.json create mode 100644 schema_json/endpoint/market_data/crypto/historical/xbbo/xbbo.json create mode 100644 schema_json/endpoint/market_data/crypto/historical/xbbo/xbbo_response.json rename schema_json/endpoint/market_data/{ => stock}/historical/bar/bar.json (100%) rename schema_json/endpoint/market_data/{ => stock}/historical/bar/bars_response.json (81%) rename schema_json/endpoint/market_data/{ => stock}/historical/bar/enums/bar_adjustment.json (100%) rename schema_json/endpoint/market_data/{ => stock}/historical/quote/latest_quote_response.json (82%) rename schema_json/endpoint/market_data/{ => stock}/historical/quote/quote.json (100%) rename schema_json/endpoint/market_data/{ => stock}/historical/quote/quotes_response.json (80%) rename schema_json/endpoint/market_data/{ => stock}/historical/snapshot/snapshot.json (67%) rename schema_json/endpoint/market_data/{ => stock}/historical/trade/latest_trade_response.json (82%) rename schema_json/endpoint/market_data/{ => stock}/historical/trade/trade.json (100%) rename schema_json/endpoint/market_data/{ => stock}/historical/trade/trades_response.json (80%) rename schema_json/endpoint/market_data/{ => stock}/realtime/bar/bar_message.json (95%) rename schema_json/endpoint/market_data/{ => stock}/realtime/control/error_message.json (91%) rename schema_json/endpoint/market_data/{ => stock}/realtime/control/subscriptions_message.json (94%) rename schema_json/endpoint/market_data/{ => stock}/realtime/control/success_message.json (89%) rename schema_json/endpoint/market_data/{ => stock}/realtime/enums/market_data_message_type.json (100%) rename schema_json/endpoint/market_data/{ => stock}/realtime/market_data_message.json (75%) rename schema_json/endpoint/market_data/{ => stock}/realtime/quote/quote_message.json (97%) rename schema_json/endpoint/market_data/{ => stock}/realtime/symbol_message.json (89%) rename schema_json/endpoint/market_data/{ => stock}/realtime/trade/trade_message.json (96%) create mode 100644 src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/CryptoMarketDataEndpoint.java rename src/main/java/net/jacobpeterson/alpaca/rest/endpoint/{MarketDataEndpoint.java => marketdata/StockMarketDataEndpoint.java} (86%) diff --git a/README.md b/README.md index de6fdc01..a0947ec0 100644 --- a/README.md +++ b/README.md @@ -100,15 +100,42 @@ try { } ``` -## [`MarketDataEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/MarketDataEndpoint.java) -The Data API v2 provides market data through an easy-to-use API for historical data. +## [`CryptoMarketDataEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/CryptoMarketDataEndpoint.java) +Alpaca provides cryptocurrency data from multiple venues/exchanges, namely: Coinbase, ErisX, and FTX. Note that it does not route orders to all venues. + +Example usage: +```java +try { + // Get BTCUSD 50 one-hour bars starting on 12/18/2021 from Coinbase and print them out + BarsResponse btcBarsResponse = alpacaAPI.cryptoMarketData().getBars( + "BTCUSD", + Arrays.asList(Exchange.COINBASE), + ZonedDateTime.of(2021, 12, 18, 0, 0, 0, 0, ZoneId.of("America/New_York")), + 50, + null, + 1, + BarTimePeriod.HOUR); + btcBarsResponse.getBars().forEach(System.out::println); + + // Get the Best Bid and Offer across multiple exchanges (XBBO) and print it out + XbboResponse etcXBBO = alpacaAPI.cryptoMarketData().getXBBO( + "ETHUSD", + null); + System.out.println(etcXBBO); +} catch (AlpacaClientException exception) { + exception.printStackTrace(); +} +``` + +## [`StockMarketDataEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/StockMarketDataEndpoint.java) +The Data API v2 provides market data through an easy-to-use API for historical stock market data. Example usage: ```java try { // Get AAPL one hour, split-adjusted bars from 7/6/2021 market open // to 7/8/2021 market close and print them out - BarsResponse aaplBarsResponse = alpacaAPI.marketData().getBars( + BarsResponse aaplBarsResponse = alpacaAPI.stockMarketData().getBars( "AAPL", ZonedDateTime.of(2021, 7, 6, 9, 30, 0, 0, ZoneId.of("America/New_York")), ZonedDateTime.of(2021, 7, 8, 12 + 4, 0, 0, 0, ZoneId.of("America/New_York")), @@ -120,7 +147,7 @@ try { aaplBarsResponse.getBars().forEach(System.out::println); // Get AAPL first 10 trades on 7/8/2021 at market open and print them out - TradesResponse aaplTradesResponse = alpacaAPI.marketData().getTrades( + TradesResponse aaplTradesResponse = alpacaAPI.stockMarketData().getTrades( "AAPL", ZonedDateTime.of(2021, 7, 8, 9, 30, 0, 0, ZoneId.of("America/New_York")), ZonedDateTime.of(2021, 7, 8, 9, 31, 0, 0, ZoneId.of("America/New_York")), @@ -129,11 +156,11 @@ try { aaplTradesResponse.getTrades().forEach(System.out::println); // Print out latest AAPL trade - Trade latestAAPLTrade = alpacaAPI.marketData().getLatestTrade("AAPL").getTrade(); + Trade latestAAPLTrade = alpacaAPI.stockMarketData().getLatestTrade("AAPL").getTrade(); System.out.printf("Latest AAPL Trade: %s\n", latestAAPLTrade); // Print out snapshot of AAPL, GME, and TSLA - Map snapshots = alpacaAPI.marketData() + Map snapshots = alpacaAPI.stockMarketData() .getSnapshots(Arrays.asList("AAPL", "GME", "TSLA")); snapshots.forEach((symbol, snapshot) -> System.out.printf("Symbol: %s\nSnapshot: %s\n\n", symbol, snapshot)); @@ -229,8 +256,8 @@ The Assets API serves as the master list of assets available for trade and data Example usage: ```java try { - // Print out a CSV of all active US equity 'Asset's - List activeUSEquities = alpacaAPI.assets().get(AssetStatus.ACTIVE, null); + // Print out a CSV of all active US Equity 'Asset's + List activeUSEquities = alpacaAPI.assets().get(AssetStatus.ACTIVE, AssetClass.US_EQUITY); System.out.println(activeUSEquities .stream().map(Asset::getSymbol) .collect(Collectors.joining(", "))); diff --git a/schema_json/endpoint/asset/asset.json b/schema_json/endpoint/asset/asset.json index b563bf4a..f5ab8b9c 100644 --- a/schema_json/endpoint/asset/asset.json +++ b/schema_json/endpoint/asset/asset.json @@ -7,8 +7,9 @@ "title": "Asset ID." }, "class": { - "existingJavaType": "java.lang.String", - "title": "\"us_equity\"" + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.asset.enums.AssetClass", + "javaName": "assetClass", + "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.asset.enums.AssetClass}." }, "exchange": { "existingJavaType": "java.lang.String", diff --git a/schema_json/endpoint/asset/enums/asset_class.json b/schema_json/endpoint/asset/enums/asset_class.json new file mode 100644 index 00000000..ac36f8e7 --- /dev/null +++ b/schema_json/endpoint/asset/enums/asset_class.json @@ -0,0 +1,8 @@ +{ + "type": "string", + "title": "See Assets.", + "enum": [ + "us_equity", + "crypto" + ] +} diff --git a/schema_json/endpoint/market_data/historical/bar/enums/bar_time_period.json b/schema_json/endpoint/market_data/common/enums/bar_time_period.json similarity index 75% rename from schema_json/endpoint/market_data/historical/bar/enums/bar_time_period.json rename to schema_json/endpoint/market_data/common/enums/bar_time_period.json index 627d9413..cccf0508 100644 --- a/schema_json/endpoint/market_data/historical/bar/enums/bar_time_period.json +++ b/schema_json/endpoint/market_data/common/enums/bar_time_period.json @@ -1,6 +1,6 @@ { "type": "string", - "title": "See Historical Data.", + "title": "See Market Data.", "enum": [ "Min", "Hour", diff --git a/schema_json/endpoint/market_data/crypto/historical/bar/bar.json b/schema_json/endpoint/market_data/crypto/historical/bar/bar.json new file mode 100644 index 00000000..c677ba92 --- /dev/null +++ b/schema_json/endpoint/market_data/crypto/historical/bar/bar.json @@ -0,0 +1,51 @@ +{ + "type": "object", + "title": "See Crypto Historical Data.", + "properties": { + "t": { + "existingJavaType": "java.time.ZonedDateTime", + "javaName": "timestamp", + "title": "Timestamp with nanosecond precision." + }, + "x": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.enums.Exchange", + "javaName": "exchange", + "title": "Exchange." + }, + "o": { + "existingJavaType": "java.lang.Double", + "javaName": "open", + "title": "Open price." + }, + "h": { + "existingJavaType": "java.lang.Double", + "javaName": "high", + "title": "High price." + }, + "l": { + "existingJavaType": "java.lang.Double", + "javaName": "low", + "title": "Low price." + }, + "c": { + "existingJavaType": "java.lang.Double", + "javaName": "close", + "title": "Close price." + }, + "v": { + "existingJavaType": "java.lang.Double", + "javaName": "volume", + "title": "Volume." + }, + "n": { + "existingJavaType": "java.lang.Long", + "javaName": "tradeCount", + "title": "Trade count." + }, + "vw": { + "existingJavaType": "java.lang.Double", + "javaName": "vwap", + "title": "VWAP (Volume Weighted Average Price)." + } + } +} diff --git a/schema_json/endpoint/market_data/crypto/historical/bar/bars_response.json b/schema_json/endpoint/market_data/crypto/historical/bar/bars_response.json new file mode 100644 index 00000000..21f5cddb --- /dev/null +++ b/schema_json/endpoint/market_data/crypto/historical/bar/bars_response.json @@ -0,0 +1,18 @@ +{ + "type": "object", + "title": "See Historical Data.", + "properties": { + "bars": { + "existingJavaType": "java.util.ArrayList", + "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.bar.Bar}s." + }, + "symbol": { + "existingJavaType": "java.lang.String", + "title": "Symbol that was queried." + }, + "next_page_token": { + "existingJavaType": "java.lang.String", + "title": "Token that can be used to query the next page." + } + } +} diff --git a/schema_json/endpoint/market_data/crypto/historical/enums/exchange.json b/schema_json/endpoint/market_data/crypto/historical/enums/exchange.json new file mode 100644 index 00000000..1ea41bd6 --- /dev/null +++ b/schema_json/endpoint/market_data/crypto/historical/enums/exchange.json @@ -0,0 +1,20 @@ +{ + "type": "string", + "title": "See Crypto Historical Data.", + "enum": [ + "ERSX", + "FTX", + "CBSE" + ], + "javaEnums": [ + { + "name": "ERISX" + }, + { + "name": "FTX" + }, + { + "name": "COINBASE" + } + ] +} diff --git a/schema_json/endpoint/market_data/crypto/historical/quote/latest_quote_response.json b/schema_json/endpoint/market_data/crypto/historical/quote/latest_quote_response.json new file mode 100644 index 00000000..044d252f --- /dev/null +++ b/schema_json/endpoint/market_data/crypto/historical/quote/latest_quote_response.json @@ -0,0 +1,14 @@ +{ + "type": "object", + "title": "See Crypto Historical Data.", + "properties": { + "symbol": { + "existingJavaType": "java.lang.String", + "title": "Symbol that was queried." + }, + "quote": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.Quote", + "title": "The latest {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.Quote}." + } + } +} diff --git a/schema_json/endpoint/market_data/crypto/historical/quote/quote.json b/schema_json/endpoint/market_data/crypto/historical/quote/quote.json new file mode 100644 index 00000000..ecca5a55 --- /dev/null +++ b/schema_json/endpoint/market_data/crypto/historical/quote/quote.json @@ -0,0 +1,36 @@ +{ + "type": "object", + "title": "See Crypto Historical Data.", + "properties": { + "t": { + "existingJavaType": "java.time.ZonedDateTime", + "javaName": "timestamp", + "title": "Timestamp with nanosecond precision." + }, + "x": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.enums.Exchange", + "javaName": "exchange", + "title": "Exchange." + }, + "ap": { + "existingJavaType": "java.lang.Double", + "javaName": "askPrice", + "title": "Ask price." + }, + "as": { + "existingJavaType": "java.lang.Double", + "javaName": "askSize", + "title": "Ask size." + }, + "bp": { + "existingJavaType": "java.lang.Double", + "javaName": "bidPrice", + "title": "Bid price." + }, + "bs": { + "existingJavaType": "java.lang.Double", + "javaName": "bidSize", + "title": "Bid size." + } + } +} diff --git a/schema_json/endpoint/market_data/crypto/historical/quote/quotes_response.json b/schema_json/endpoint/market_data/crypto/historical/quote/quotes_response.json new file mode 100644 index 00000000..212635c3 --- /dev/null +++ b/schema_json/endpoint/market_data/crypto/historical/quote/quotes_response.json @@ -0,0 +1,18 @@ +{ + "type": "object", + "title": "See Crypto Historical Data.", + "properties": { + "quotes": { + "existingJavaType": "java.util.ArrayList", + "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.Quote}s." + }, + "symbol": { + "existingJavaType": "java.lang.String", + "title": "Symbol that was queried." + }, + "next_page_token": { + "existingJavaType": "java.lang.String", + "title": "Token that can be used to query the next page." + } + } +} diff --git a/schema_json/endpoint/market_data/crypto/historical/trade/enums/taker_side.json b/schema_json/endpoint/market_data/crypto/historical/trade/enums/taker_side.json new file mode 100644 index 00000000..9604bc54 --- /dev/null +++ b/schema_json/endpoint/market_data/crypto/historical/trade/enums/taker_side.json @@ -0,0 +1,16 @@ +{ + "type": "string", + "title": "See Crypto Historical Data.", + "enum": [ + "B", + "S" + ], + "javaEnums": [ + { + "name": "BUY" + }, + { + "name": "SELL" + } + ] +} diff --git a/schema_json/endpoint/market_data/crypto/historical/trade/latest_trade_response.json b/schema_json/endpoint/market_data/crypto/historical/trade/latest_trade_response.json new file mode 100644 index 00000000..089f9528 --- /dev/null +++ b/schema_json/endpoint/market_data/crypto/historical/trade/latest_trade_response.json @@ -0,0 +1,14 @@ +{ + "type": "object", + "title": "See Crypto Historical Data.", + "properties": { + "symbol": { + "existingJavaType": "java.lang.String", + "title": "Symbol that was queried." + }, + "trade": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.Trade", + "title": "The latest {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.Trade}." + } + } +} diff --git a/schema_json/endpoint/market_data/crypto/historical/trade/trade.json b/schema_json/endpoint/market_data/crypto/historical/trade/trade.json new file mode 100644 index 00000000..56fef69b --- /dev/null +++ b/schema_json/endpoint/market_data/crypto/historical/trade/trade.json @@ -0,0 +1,36 @@ +{ + "type": "object", + "title": "See Crypto Historical Data.", + "properties": { + "t": { + "existingJavaType": "java.time.ZonedDateTime", + "javaName": "timestamp", + "title": "Timestamp with nanosecond precision." + }, + "x": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.enums.Exchange", + "javaName": "exchange", + "title": "Exchange where the trade happened." + }, + "p": { + "existingJavaType": "java.lang.Double", + "javaName": "price", + "title": "Trade price." + }, + "s": { + "existingJavaType": "java.lang.Double", + "javaName": "size", + "title": "Trade size." + }, + "tks": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.enums.TakerSide", + "javaName": "takerSide", + "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.enums.TakerSide} of the trade." + }, + "i": { + "existingJavaType": "java.lang.Long", + "javaName": "tradeID", + "title": "Trade ID." + } + } +} diff --git a/schema_json/endpoint/market_data/crypto/historical/trade/trades_response.json b/schema_json/endpoint/market_data/crypto/historical/trade/trades_response.json new file mode 100644 index 00000000..68a770a5 --- /dev/null +++ b/schema_json/endpoint/market_data/crypto/historical/trade/trades_response.json @@ -0,0 +1,18 @@ +{ + "type": "object", + "title": "See Crypto Historical Data.", + "properties": { + "trades": { + "existingJavaType": "java.util.ArrayList", + "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.Trade}s." + }, + "symbol": { + "existingJavaType": "java.lang.String", + "title": "Symbol that was queried." + }, + "next_page_token": { + "existingJavaType": "java.lang.String", + "title": "Token that can be used to query the next page." + } + } +} diff --git a/schema_json/endpoint/market_data/crypto/historical/xbbo/xbbo.json b/schema_json/endpoint/market_data/crypto/historical/xbbo/xbbo.json new file mode 100644 index 00000000..9646a1ee --- /dev/null +++ b/schema_json/endpoint/market_data/crypto/historical/xbbo/xbbo.json @@ -0,0 +1,41 @@ +{ + "type": "object", + "title": "See Crypto Historical Data.", + "properties": { + "t": { + "existingJavaType": "java.time.ZonedDateTime", + "javaName": "timestamp", + "title": "Timestamp with nanosecond precision." + }, + "ax": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.enums.Exchange", + "javaName": "askExchange", + "title": "Ask Exchange." + }, + "ap": { + "existingJavaType": "java.lang.Double", + "javaName": "askPrice", + "title": "Ask price." + }, + "as": { + "existingJavaType": "java.lang.Double", + "javaName": "askSize", + "title": "Ask size." + }, + "bx": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.enums.Exchange", + "javaName": "bidExchange", + "title": "Bid Exchange." + }, + "bp": { + "existingJavaType": "java.lang.Double", + "javaName": "bidPrice", + "title": "Bid price." + }, + "bs": { + "existingJavaType": "java.lang.Double", + "javaName": "bidSize", + "title": "Bid size." + } + } +} diff --git a/schema_json/endpoint/market_data/crypto/historical/xbbo/xbbo_response.json b/schema_json/endpoint/market_data/crypto/historical/xbbo/xbbo_response.json new file mode 100644 index 00000000..b3bbe427 --- /dev/null +++ b/schema_json/endpoint/market_data/crypto/historical/xbbo/xbbo_response.json @@ -0,0 +1,14 @@ +{ + "type": "object", + "title": "See Crypto Historical Data.", + "properties": { + "symbol": { + "existingJavaType": "java.lang.String", + "title": "Symbol that was queried." + }, + "xbbo": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.xbbo.Xbbo", + "title": "The latest {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.xbbo.Xbbo}." + } + } +} diff --git a/schema_json/endpoint/market_data/historical/bar/bar.json b/schema_json/endpoint/market_data/stock/historical/bar/bar.json similarity index 100% rename from schema_json/endpoint/market_data/historical/bar/bar.json rename to schema_json/endpoint/market_data/stock/historical/bar/bar.json diff --git a/schema_json/endpoint/market_data/historical/bar/bars_response.json b/schema_json/endpoint/market_data/stock/historical/bar/bars_response.json similarity index 81% rename from schema_json/endpoint/market_data/historical/bar/bars_response.json rename to schema_json/endpoint/market_data/stock/historical/bar/bars_response.json index a876dcf8..ebd21e82 100644 --- a/schema_json/endpoint/market_data/historical/bar/bars_response.json +++ b/schema_json/endpoint/market_data/stock/historical/bar/bars_response.json @@ -3,8 +3,8 @@ "title": "See Historical Data.", "properties": { "bars": { - "existingJavaType": "java.util.ArrayList", - "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.bar.Bar}s." + "existingJavaType": "java.util.ArrayList", + "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.Bar}s." }, "symbol": { "existingJavaType": "java.lang.String", diff --git a/schema_json/endpoint/market_data/historical/bar/enums/bar_adjustment.json b/schema_json/endpoint/market_data/stock/historical/bar/enums/bar_adjustment.json similarity index 100% rename from schema_json/endpoint/market_data/historical/bar/enums/bar_adjustment.json rename to schema_json/endpoint/market_data/stock/historical/bar/enums/bar_adjustment.json diff --git a/schema_json/endpoint/market_data/historical/quote/latest_quote_response.json b/schema_json/endpoint/market_data/stock/historical/quote/latest_quote_response.json similarity index 82% rename from schema_json/endpoint/market_data/historical/quote/latest_quote_response.json rename to schema_json/endpoint/market_data/stock/historical/quote/latest_quote_response.json index 54477417..1250c5f8 100644 --- a/schema_json/endpoint/market_data/historical/quote/latest_quote_response.json +++ b/schema_json/endpoint/market_data/stock/historical/quote/latest_quote_response.json @@ -7,8 +7,8 @@ "title": "Symbol that was queried." }, "quote": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.quote.Quote", - "title": "The latest {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.quote.Quote}." + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.Quote", + "title": "The latest {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.Quote}." } } } diff --git a/schema_json/endpoint/market_data/historical/quote/quote.json b/schema_json/endpoint/market_data/stock/historical/quote/quote.json similarity index 100% rename from schema_json/endpoint/market_data/historical/quote/quote.json rename to schema_json/endpoint/market_data/stock/historical/quote/quote.json diff --git a/schema_json/endpoint/market_data/historical/quote/quotes_response.json b/schema_json/endpoint/market_data/stock/historical/quote/quotes_response.json similarity index 80% rename from schema_json/endpoint/market_data/historical/quote/quotes_response.json rename to schema_json/endpoint/market_data/stock/historical/quote/quotes_response.json index 88c1286e..6ba96682 100644 --- a/schema_json/endpoint/market_data/historical/quote/quotes_response.json +++ b/schema_json/endpoint/market_data/stock/historical/quote/quotes_response.json @@ -3,8 +3,8 @@ "title": "See Historical Data.", "properties": { "quotes": { - "existingJavaType": "java.util.ArrayList", - "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.quote.Quote}s." + "existingJavaType": "java.util.ArrayList", + "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.Quote}s." }, "symbol": { "existingJavaType": "java.lang.String", diff --git a/schema_json/endpoint/market_data/historical/snapshot/snapshot.json b/schema_json/endpoint/market_data/stock/historical/snapshot/snapshot.json similarity index 67% rename from schema_json/endpoint/market_data/historical/snapshot/snapshot.json rename to schema_json/endpoint/market_data/stock/historical/snapshot/snapshot.json index c2a0acf0..f42b18af 100644 --- a/schema_json/endpoint/market_data/historical/snapshot/snapshot.json +++ b/schema_json/endpoint/market_data/stock/historical/snapshot/snapshot.json @@ -3,24 +3,24 @@ "title": "See Historical Data.", "properties": { "latestTrade": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.trade.Trade", - "title": "The latest {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.trade.Trade}." + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.Trade", + "title": "The latest {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.Trade}." }, "latestQuote": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.quote.Quote", - "title": "The latest {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.quote.Quote}." + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.Quote", + "title": "The latest {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.Quote}." }, "minuteBar": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.bar.Bar", - "title": "The minute {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.bar.Bar}." + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.Bar", + "title": "The minute {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.Bar}." }, "dailyBar": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.bar.Bar", - "title": "The daily {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.bar.Bar}." + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.Bar", + "title": "The daily {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.Bar}." }, "prevDailyBar": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.bar.Bar", - "title": "The previous daily {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.bar.Bar}." + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.Bar", + "title": "The previous daily {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.Bar}." } } } diff --git a/schema_json/endpoint/market_data/historical/trade/latest_trade_response.json b/schema_json/endpoint/market_data/stock/historical/trade/latest_trade_response.json similarity index 82% rename from schema_json/endpoint/market_data/historical/trade/latest_trade_response.json rename to schema_json/endpoint/market_data/stock/historical/trade/latest_trade_response.json index 3025a719..f5be1b9d 100644 --- a/schema_json/endpoint/market_data/historical/trade/latest_trade_response.json +++ b/schema_json/endpoint/market_data/stock/historical/trade/latest_trade_response.json @@ -7,8 +7,8 @@ "title": "Symbol that was queried." }, "trade": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.trade.Trade", - "title": "The latest {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.trade.Trade}." + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.Trade", + "title": "The latest {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.Trade}." } } } diff --git a/schema_json/endpoint/market_data/historical/trade/trade.json b/schema_json/endpoint/market_data/stock/historical/trade/trade.json similarity index 100% rename from schema_json/endpoint/market_data/historical/trade/trade.json rename to schema_json/endpoint/market_data/stock/historical/trade/trade.json diff --git a/schema_json/endpoint/market_data/historical/trade/trades_response.json b/schema_json/endpoint/market_data/stock/historical/trade/trades_response.json similarity index 80% rename from schema_json/endpoint/market_data/historical/trade/trades_response.json rename to schema_json/endpoint/market_data/stock/historical/trade/trades_response.json index 928f6f24..503b6d9f 100644 --- a/schema_json/endpoint/market_data/historical/trade/trades_response.json +++ b/schema_json/endpoint/market_data/stock/historical/trade/trades_response.json @@ -3,8 +3,8 @@ "title": "See Historical Data.", "properties": { "trades": { - "existingJavaType": "java.util.ArrayList", - "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.trade.Trade}s." + "existingJavaType": "java.util.ArrayList", + "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.Trade}s." }, "symbol": { "existingJavaType": "java.lang.String", diff --git a/schema_json/endpoint/market_data/realtime/bar/bar_message.json b/schema_json/endpoint/market_data/stock/realtime/bar/bar_message.json similarity index 95% rename from schema_json/endpoint/market_data/realtime/bar/bar_message.json rename to schema_json/endpoint/market_data/stock/realtime/bar/bar_message.json index cc6aa003..7abf464d 100644 --- a/schema_json/endpoint/market_data/realtime/bar/bar_message.json +++ b/schema_json/endpoint/market_data/stock/realtime/bar/bar_message.json @@ -1,7 +1,7 @@ { "type": "object", "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.realtime.SymbolMessage" + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.SymbolMessage" }, "title": "See Real-time Data.", "properties": { diff --git a/schema_json/endpoint/market_data/realtime/control/error_message.json b/schema_json/endpoint/market_data/stock/realtime/control/error_message.json similarity index 91% rename from schema_json/endpoint/market_data/realtime/control/error_message.json rename to schema_json/endpoint/market_data/stock/realtime/control/error_message.json index 6dca33c4..1ef275ab 100644 --- a/schema_json/endpoint/market_data/realtime/control/error_message.json +++ b/schema_json/endpoint/market_data/stock/realtime/control/error_message.json @@ -1,7 +1,7 @@ { "type": "object", "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.realtime.MarketDataMessage" + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.MarketDataMessage" }, "title": "See Real-time Data.", "properties": { diff --git a/schema_json/endpoint/market_data/realtime/control/subscriptions_message.json b/schema_json/endpoint/market_data/stock/realtime/control/subscriptions_message.json similarity index 94% rename from schema_json/endpoint/market_data/realtime/control/subscriptions_message.json rename to schema_json/endpoint/market_data/stock/realtime/control/subscriptions_message.json index 6602ed55..76bfc479 100644 --- a/schema_json/endpoint/market_data/realtime/control/subscriptions_message.json +++ b/schema_json/endpoint/market_data/stock/realtime/control/subscriptions_message.json @@ -1,7 +1,7 @@ { "type": "object", "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.realtime.MarketDataMessage" + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.MarketDataMessage" }, "title": "See Real-time Data.", "properties": { diff --git a/schema_json/endpoint/market_data/realtime/control/success_message.json b/schema_json/endpoint/market_data/stock/realtime/control/success_message.json similarity index 89% rename from schema_json/endpoint/market_data/realtime/control/success_message.json rename to schema_json/endpoint/market_data/stock/realtime/control/success_message.json index 76ba8a8c..d7225e02 100644 --- a/schema_json/endpoint/market_data/realtime/control/success_message.json +++ b/schema_json/endpoint/market_data/stock/realtime/control/success_message.json @@ -1,7 +1,7 @@ { "type": "object", "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.realtime.MarketDataMessage" + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.MarketDataMessage" }, "title": "See Real-time Data.", "properties": { diff --git a/schema_json/endpoint/market_data/realtime/enums/market_data_message_type.json b/schema_json/endpoint/market_data/stock/realtime/enums/market_data_message_type.json similarity index 100% rename from schema_json/endpoint/market_data/realtime/enums/market_data_message_type.json rename to schema_json/endpoint/market_data/stock/realtime/enums/market_data_message_type.json diff --git a/schema_json/endpoint/market_data/realtime/market_data_message.json b/schema_json/endpoint/market_data/stock/realtime/market_data_message.json similarity index 75% rename from schema_json/endpoint/market_data/realtime/market_data_message.json rename to schema_json/endpoint/market_data/stock/realtime/market_data_message.json index 3e43d789..5ee64bef 100644 --- a/schema_json/endpoint/market_data/realtime/market_data_message.json +++ b/schema_json/endpoint/market_data/stock/realtime/market_data_message.json @@ -3,9 +3,9 @@ "title": "See Real-time Data.", "properties": { "T": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.realtime.enums.MarketDataMessageType", + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.enums.MarketDataMessageType", "javaName": "messageType", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.realtime.enums.MarketDataMessageType}." + "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.enums.MarketDataMessageType}." } } } diff --git a/schema_json/endpoint/market_data/realtime/quote/quote_message.json b/schema_json/endpoint/market_data/stock/realtime/quote/quote_message.json similarity index 97% rename from schema_json/endpoint/market_data/realtime/quote/quote_message.json rename to schema_json/endpoint/market_data/stock/realtime/quote/quote_message.json index f1f9606d..3c7c650c 100644 --- a/schema_json/endpoint/market_data/realtime/quote/quote_message.json +++ b/schema_json/endpoint/market_data/stock/realtime/quote/quote_message.json @@ -1,7 +1,7 @@ { "type": "object", "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.realtime.SymbolMessage" + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.SymbolMessage" }, "title": "See Real-time Data.", "properties": { diff --git a/schema_json/endpoint/market_data/realtime/symbol_message.json b/schema_json/endpoint/market_data/stock/realtime/symbol_message.json similarity index 89% rename from schema_json/endpoint/market_data/realtime/symbol_message.json rename to schema_json/endpoint/market_data/stock/realtime/symbol_message.json index 961d15c0..acc6327c 100644 --- a/schema_json/endpoint/market_data/realtime/symbol_message.json +++ b/schema_json/endpoint/market_data/stock/realtime/symbol_message.json @@ -1,7 +1,7 @@ { "type": "object", "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.realtime.MarketDataMessage" + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.MarketDataMessage" }, "title": "See Real-time Data.", "properties": { diff --git a/schema_json/endpoint/market_data/realtime/trade/trade_message.json b/schema_json/endpoint/market_data/stock/realtime/trade/trade_message.json similarity index 96% rename from schema_json/endpoint/market_data/realtime/trade/trade_message.json rename to schema_json/endpoint/market_data/stock/realtime/trade/trade_message.json index 7d01e2b8..e72f5859 100644 --- a/schema_json/endpoint/market_data/realtime/trade/trade_message.json +++ b/schema_json/endpoint/market_data/stock/realtime/trade/trade_message.json @@ -1,7 +1,7 @@ { "type": "object", "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.realtime.SymbolMessage" + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.SymbolMessage" }, "title": "See Real-time Data.", "properties": { diff --git a/schema_json/endpoint/position/position.json b/schema_json/endpoint/position/position.json index 2f1eb603..7ebb619f 100644 --- a/schema_json/endpoint/position/position.json +++ b/schema_json/endpoint/position/position.json @@ -12,7 +12,7 @@ }, "exchange": { "existingJavaType": "java.lang.String", - "title": "Exchange name of the asset." + "title": "Exchange name of the asset (\"ErisX\" for crypto)." }, "asset_class": { "existingJavaType": "java.lang.String", diff --git a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java index b874d794..67813131 100644 --- a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java +++ b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java @@ -12,11 +12,12 @@ import net.jacobpeterson.alpaca.rest.endpoint.AssetsEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.CalendarEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.ClockEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.MarketDataEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.OrdersEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.PortfolioHistoryEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.PositionsEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.WatchlistEndpoint; +import net.jacobpeterson.alpaca.rest.endpoint.marketdata.CryptoMarketDataEndpoint; +import net.jacobpeterson.alpaca.rest.endpoint.marketdata.StockMarketDataEndpoint; import net.jacobpeterson.alpaca.websocket.AlpacaWebsocket; import net.jacobpeterson.alpaca.websocket.marketdata.MarketDataWebsocket; import net.jacobpeterson.alpaca.websocket.marketdata.MarketDataWebsocketInterface; @@ -42,13 +43,16 @@ public class AlpacaAPI { private static final Logger LOGGER = LoggerFactory.getLogger(AlpacaAPI.class); private static final String VERSION_2_PATH_SEGMENT = "v2"; + private static final String VERSION_1_BETA_1_PATH_SEGMENT = "v1beta1"; private final OkHttpClient okHttpClient; private final AlpacaClient brokerClient; - private final AlpacaClient dataClient; + private final AlpacaClient cryptoDataClient; + private final AlpacaClient stockDataClient; // Ordering of fields/methods below are analogous to the ordering in the Alpaca documentation private final AccountEndpoint accountEndpoint; - private final MarketDataEndpoint marketDataEndpoint; + private final CryptoMarketDataEndpoint cryptoMarketDataEndpoint; + private final StockMarketDataEndpoint stockMarketDataEndpoint; private final OrdersEndpoint ordersEndpoint; private final PositionsEndpoint positionsEndpoint; private final AssetsEndpoint assetsEndpoint; @@ -154,14 +158,17 @@ public AlpacaAPI(OkHttpClient okHttpClient, String keyID, String secretKey, Stri if (oAuthToken == null) { brokerClient = new AlpacaClient(okHttpClient, keyID, secretKey, brokerHostSubdomain, VERSION_2_PATH_SEGMENT); - dataClient = new AlpacaClient(okHttpClient, keyID, secretKey, "data", VERSION_2_PATH_SEGMENT); + cryptoDataClient = new AlpacaClient(okHttpClient, keyID, secretKey, "data", VERSION_1_BETA_1_PATH_SEGMENT); + stockDataClient = new AlpacaClient(okHttpClient, keyID, secretKey, "data", VERSION_2_PATH_SEGMENT); } else { brokerClient = new AlpacaClient(okHttpClient, oAuthToken, brokerHostSubdomain, VERSION_2_PATH_SEGMENT); - dataClient = null; + cryptoDataClient = null; + stockDataClient = null; } accountEndpoint = new AccountEndpoint(brokerClient); - marketDataEndpoint = dataClient == null ? null : new MarketDataEndpoint(dataClient); + cryptoMarketDataEndpoint = cryptoDataClient == null ? null : new CryptoMarketDataEndpoint(cryptoDataClient); + stockMarketDataEndpoint = stockDataClient == null ? null : new StockMarketDataEndpoint(stockDataClient); ordersEndpoint = new OrdersEndpoint(brokerClient); positionsEndpoint = new PositionsEndpoint(brokerClient); assetsEndpoint = new AssetsEndpoint(brokerClient); @@ -172,7 +179,7 @@ public AlpacaAPI(OkHttpClient okHttpClient, String keyID, String secretKey, Stri accountActivitiesEndpoint = new AccountActivitiesEndpoint(brokerClient); portfolioHistoryEndpoint = new PortfolioHistoryEndpoint(brokerClient); streamingWebsocket = new StreamingWebsocket(okHttpClient, brokerHostSubdomain, keyID, secretKey, oAuthToken); - marketDataWebsocket = dataClient == null ? null : + marketDataWebsocket = stockDataClient == null ? null : new MarketDataWebsocket(okHttpClient, dataAPIType, keyID, secretKey); } @@ -184,10 +191,17 @@ public AccountEndpoint account() { } /** - * @return the {@link MarketDataEndpoint} + * @return the {@link CryptoMarketDataEndpoint} */ - public MarketDataEndpoint marketData() { - return marketDataEndpoint; + public CryptoMarketDataEndpoint cryptoMarketData() { + return cryptoMarketDataEndpoint; + } + + /** + * @return the {@link StockMarketDataEndpoint} + */ + public StockMarketDataEndpoint stockMarketData() { + return stockMarketDataEndpoint; } /** @@ -267,30 +281,19 @@ public MarketDataWebsocketInterface marketDataStreaming() { return marketDataWebsocket; } - /** - * Gets {@link #okHttpClient}. - * - * @return the {@link OkHttpClient} - */ public OkHttpClient getOkHttpClient() { return okHttpClient; } - /** - * Gets {@link #brokerClient}. - * - * @return the broker {@link AlpacaClient} - */ public AlpacaClient getBrokerClient() { return brokerClient; } - /** - * Gets {@link #dataClient}. - * - * @return the data {@link AlpacaClient} - */ - public AlpacaClient getDataClient() { - return dataClient; + public AlpacaClient getCryptoDataClient() { + return cryptoDataClient; + } + + public AlpacaClient getStockDataClient() { + return stockDataClient; } } diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClient.java b/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClient.java index 90e7265a..12cb6a6f 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClient.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClient.java @@ -222,20 +222,10 @@ public Response executeRequest(Request request) throws IOException { return okHttpClient.newCall(request).execute(); } - /** - * Gets {@link #baseURL}. - * - * @return the base {@link HttpUrl} - */ public HttpUrl getBaseURL() { return baseURL; } - /** - * Gets {@link #requestHeaders}. - * - * @return the request {@link Headers} - */ public Headers getRequestHeaders() { return requestHeaders; } diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClientException.java b/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClientException.java index bcf89c2b..2114b1e8 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClientException.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClientException.java @@ -120,38 +120,18 @@ private String parseAPIErrorResponse() { return messageBuilder.toString(); } - /** - * Gets the {@link #responseStatusCode}. - * - * @return an {@link Integer} - */ public Integer getResponseStatusCode() { return responseStatusCode; } - /** - * Gets the {@link #responseStatusMessage}. - * - * @return a {@link String} - */ public String getResponseStatusMessage() { return responseStatusMessage; } - /** - * Gets the {@link #apiResponseCode}. - * - * @return an {@link Integer} - */ public Integer getAPIResponseCode() { return apiResponseCode; } - /** - * Gets the {@link #apiResponseMessage}. - * - * @return a {@link String} - */ public String getAPIResponseMessage() { return apiResponseMessage; } diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AccountActivitiesEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AccountActivitiesEndpoint.java index d1e5b88b..adbe8414 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AccountActivitiesEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AccountActivitiesEndpoint.java @@ -56,7 +56,7 @@ public AccountActivitiesEndpoint(AlpacaClient alpacaClient) { * @param sortDirection the {@link SortDirection} (defaults to {@link SortDirection#DESCENDING} if unspecified.) * @param pageSize the maximum number of entries to return in the response. (See the section on paging.) * @param pageToken the ID of the end of your current page of results. (See the section on paging.) - * @param activityTypes the {@link ActivityType}s (null for all {@link ActivityType}s) + * @param activityTypes the {@link ActivityType}s (null for all {@link ActivityType}s) * * @return a {@link List} of {@link AccountActivity}s * diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AssetsEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AssetsEndpoint.java index c0642115..6cbf3298 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AssetsEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AssetsEndpoint.java @@ -2,6 +2,7 @@ import com.google.gson.reflect.TypeToken; import net.jacobpeterson.alpaca.model.endpoint.asset.Asset; +import net.jacobpeterson.alpaca.model.endpoint.asset.enums.AssetClass; import net.jacobpeterson.alpaca.model.endpoint.asset.enums.AssetStatus; import net.jacobpeterson.alpaca.rest.AlpacaClient; import net.jacobpeterson.alpaca.rest.AlpacaClientException; @@ -31,13 +32,13 @@ public AssetsEndpoint(AlpacaClient alpacaClient) { * Get a list of {@link Asset}s. * * @param assetStatus the {@link AssetStatus}. By default, all {@link AssetStatus}es are included. - * @param assetClass the asset class. Defaults to "us_equity". + * @param assetClass the {@link AssetClass} (null for {@link AssetClass#US_EQUITY}) * * @return a {@link List} of {@link Asset}s * * @throws AlpacaClientException thrown for {@link AlpacaClientException}s */ - public List get(AssetStatus assetStatus, String assetClass) throws AlpacaClientException { + public List get(AssetStatus assetStatus, AssetClass assetClass) throws AlpacaClientException { HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() .addPathSegment(endpointPathSegment); @@ -46,7 +47,7 @@ public List get(AssetStatus assetStatus, String assetClass) throws Alpaca } if (assetClass != null) { - urlBuilder.addQueryParameter("asset_class", assetClass); + urlBuilder.addQueryParameter("asset_class", assetClass.toString()); } Request request = alpacaClient.requestBuilder(urlBuilder.build()) diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/OrdersEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/OrdersEndpoint.java index a542d12d..8715d1fc 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/OrdersEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/OrdersEndpoint.java @@ -52,7 +52,9 @@ public OrdersEndpoint(AlpacaClient alpacaClient) { * @param direction the chronological order of response based on the submission time. Defaults to {@link * SortDirection#DESCENDING}. * @param nested if true, the result will roll up multi-leg orders under the legs field of primary order. - * @param symbols a {@link Collection} of symbols to filter the result by (null for no filter). + * @param symbols a {@link Collection} of symbols to filter by (e.g. "AAPL,TSLA,MSFT"). A currency pair is + * required for crypto orders (e.g. "BTCUSD,BCHUSD,LTCUSD,ETCUSD"). null for no + * filter. * * @return a {@link List} of {@link Order}s * diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/CryptoMarketDataEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/CryptoMarketDataEndpoint.java new file mode 100644 index 00000000..c044d1f8 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/CryptoMarketDataEndpoint.java @@ -0,0 +1,296 @@ +package net.jacobpeterson.alpaca.rest.endpoint.marketdata; + +import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.enums.BarTimePeriod; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.bar.Bar; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.bar.BarsResponse; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.enums.Exchange; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.LatestQuoteResponse; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.Quote; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.QuotesResponse; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.LatestTradeResponse; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.Trade; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.TradesResponse; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.xbbo.XbboResponse; +import net.jacobpeterson.alpaca.rest.AlpacaClient; +import net.jacobpeterson.alpaca.rest.AlpacaClientException; +import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; +import net.jacobpeterson.alpaca.util.format.FormatUtil; +import okhttp3.HttpUrl; +import okhttp3.Request; + +import java.time.ZonedDateTime; +import java.util.Collection; +import java.util.stream.Collectors; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * {@link AlpacaEndpoint} for + * Historical + * Crypto Market Data API. + */ +public class CryptoMarketDataEndpoint extends AlpacaEndpoint { + + /** + * Instantiates a new {@link CryptoMarketDataEndpoint}. + * + * @param alpacaClient the {@link AlpacaClient} + */ + public CryptoMarketDataEndpoint(AlpacaClient alpacaClient) { + super(alpacaClient, "crypto"); + } + + /** + * Gets {@link Trade} historical data for the requested crypto symbol. + * + * @param symbol the symbol to query for + * @param exchanges a {@link Collection} of {@link Exchange}s to filter by. null for all exchanges. + * @param start filter data equal to or after this {@link ZonedDateTime}. Fractions of a second are not + * accepted. null for the current day in Central Time. + * @param end filter data equal to or before this {@link ZonedDateTime}. Fractions of a second are not + * accepted. null for now. + * @param limit number of data points to return. Must be in range 1-10000, defaults to 1000 if null + * is given + * @param pageToken pagination token to continue from + * + * @return the {@link TradesResponse} + * + * @throws AlpacaClientException thrown for {@link AlpacaClientException}s + */ + public TradesResponse getTrades(String symbol, Collection exchanges, ZonedDateTime start, + ZonedDateTime end, Integer limit, String pageToken) throws AlpacaClientException { + checkNotNull(symbol); + + HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() + .addPathSegment(endpointPathSegment) + .addPathSegment(symbol) + .addPathSegment("trades"); + + if (exchanges != null) { + urlBuilder.addQueryParameter("exchanges", exchanges.stream() + .map(Exchange::value) + .collect(Collectors.joining(","))); + } + + if (start != null) { + urlBuilder.addQueryParameter("start", FormatUtil.toRFC3339Format(start)); + } + + if (end != null) { + urlBuilder.addQueryParameter("end", FormatUtil.toRFC3339Format(end)); + } + + if (limit != null) { + urlBuilder.addQueryParameter("limit", limit.toString()); + } + + if (pageToken != null) { + urlBuilder.addQueryParameter("page_token", pageToken); + } + + Request request = alpacaClient.requestBuilder(urlBuilder.build()) + .get() + .build(); + return alpacaClient.requestObject(request, TradesResponse.class); + } + + /** + * Gets the latest {@link Trade} for the requested security. + * + * @param symbol the symbol to query for + * @param exchange the {@link Exchange} to filter the latest {@link Trade} by. + * + * @return the {@link LatestTradeResponse} + * + * @throws AlpacaClientException thrown for {@link AlpacaClientException}s + */ + public LatestTradeResponse getLatestTrade(String symbol, Exchange exchange) throws AlpacaClientException { + checkNotNull(symbol); + checkNotNull(exchange); + + HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() + .addPathSegment(endpointPathSegment) + .addPathSegment(symbol) + .addPathSegment("trades") + .addPathSegment("latest"); + + urlBuilder.addQueryParameter("exchange", exchange.toString()); + + Request request = alpacaClient.requestBuilder(urlBuilder.build()) + .get() + .build(); + return alpacaClient.requestObject(request, LatestTradeResponse.class); + } + + /** + * Gets {@link Quote} historical data for the requested crypto symbol. + * + * @param symbol the symbol to query for + * @param exchanges a {@link Collection} of {@link Exchange}s to filter by. null for all exchanges. + * @param start filter data equal to or after this {@link ZonedDateTime}. Fractions of a second are not + * accepted. null for the current day in Central Time. + * @param end filter data equal to or before this {@link ZonedDateTime}. Fractions of a second are not + * accepted. null for now. + * @param limit number of data points to return. Must be in range 1-10000, defaults to 1000 if null + * is given + * @param pageToken pagination token to continue from + * + * @return the {@link QuotesResponse} + * + * @throws AlpacaClientException thrown for {@link AlpacaClientException}s + */ + public QuotesResponse getQuotes(String symbol, Collection exchanges, ZonedDateTime start, + ZonedDateTime end, Integer limit, String pageToken) throws AlpacaClientException { + checkNotNull(symbol); + + HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() + .addPathSegment(endpointPathSegment) + .addPathSegment(symbol) + .addPathSegment("quotes"); + + if (exchanges != null) { + urlBuilder.addQueryParameter("exchanges", exchanges.stream() + .map(Exchange::value) + .collect(Collectors.joining(","))); + } + + if (start != null) { + urlBuilder.addQueryParameter("start", FormatUtil.toRFC3339Format(start)); + } + + if (end != null) { + urlBuilder.addQueryParameter("end", FormatUtil.toRFC3339Format(end)); + } + + if (limit != null) { + urlBuilder.addQueryParameter("limit", limit.toString()); + } + + if (pageToken != null) { + urlBuilder.addQueryParameter("page_token", pageToken); + } + + Request request = alpacaClient.requestBuilder(urlBuilder.build()) + .get() + .build(); + return alpacaClient.requestObject(request, QuotesResponse.class); + } + + /** + * Gets the latest {@link Quote} for the requested security. + * + * @param symbol the symbol to query for + * @param exchange the {@link Exchange} to filter the latest {@link Quote} by. + * + * @return the {@link LatestQuoteResponse} + * + * @throws AlpacaClientException thrown for {@link AlpacaClientException}s + */ + public LatestQuoteResponse getLatestQuote(String symbol, Exchange exchange) throws AlpacaClientException { + checkNotNull(symbol); + checkNotNull(exchange); + + HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() + .addPathSegment(endpointPathSegment) + .addPathSegment(symbol) + .addPathSegment("quotes") + .addPathSegment("latest"); + + urlBuilder.addQueryParameter("exchange", exchange.toString()); + + Request request = alpacaClient.requestBuilder(urlBuilder.build()) + .get() + .build(); + return alpacaClient.requestObject(request, LatestQuoteResponse.class); + } + + /** + * Gets {@link Bar} aggregate historical data for the requested crypto. + * + * @param symbol the symbol to query for + * @param exchanges a {@link Collection} of {@link Exchange}s to filter by. null for all + * exchanges. + * @param start filter data equal to or after this {@link ZonedDateTime}. Fractions of a second are + * not accepted. null for now. + * @param limit number of data points to return. Must be in range 1-10000, defaults to 1000 if + * null is given + * @param pageToken pagination token to continue from + * @param barTimePeriodDuration the duration for the given barTimePeriod parameter. e.g. for + * 15Min bars, you would supply 15 for this parameter and + * {@link BarTimePeriod#MINUTE} for the barTimePeriod parameter. + * @param barTimePeriod the {@link BarTimePeriod} e.g. for 15Min bars, you would supply {@link + * BarTimePeriod#MINUTE} for this parameter and 15 for the + * barTimePeriodDuration parameter. + * + * @return the {@link BarsResponse} + * + * @throws AlpacaClientException thrown for {@link AlpacaClientException}s + */ + public BarsResponse getBars(String symbol, Collection exchanges, ZonedDateTime start, Integer limit, + String pageToken, int barTimePeriodDuration, BarTimePeriod barTimePeriod) throws AlpacaClientException { + checkNotNull(symbol); + checkNotNull(barTimePeriod); + + HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() + .addPathSegment(endpointPathSegment) + .addPathSegment(symbol) + .addPathSegment("bars"); + + if (exchanges != null) { + urlBuilder.addQueryParameter("exchanges", exchanges.stream() + .map(Exchange::value) + .collect(Collectors.joining(","))); + } + + if (start != null) { + urlBuilder.addQueryParameter("start", FormatUtil.toRFC3339Format(start)); + } + + if (limit != null) { + urlBuilder.addQueryParameter("limit", limit.toString()); + } + + if (pageToken != null) { + urlBuilder.addQueryParameter("page_token", pageToken); + } + + urlBuilder.addQueryParameter("timeframe", barTimePeriodDuration + barTimePeriod.toString()); + + Request request = alpacaClient.requestBuilder(urlBuilder.build()) + .get() + .build(); + return alpacaClient.requestObject(request, BarsResponse.class); + } + + /** + * Returns the XBBO for a crypto symbol that calculates the Best Bid and Offer across multiple exchanges. + * + * @param symbol the symbol to query for + * @param exchanges a {@link Collection} of {@link Exchange}s to filter by. If null, then only the + * exchanges that can be traded on Alpaca are included in the calculation. + * + * @return the {@link LatestQuoteResponse} + * + * @throws AlpacaClientException thrown for {@link AlpacaClientException}s + */ + public XbboResponse getXBBO(String symbol, Collection exchanges) throws AlpacaClientException { + checkNotNull(symbol); + + HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() + .addPathSegment(endpointPathSegment) + .addPathSegment(symbol) + .addPathSegment("xbbo") + .addPathSegment("latest"); + + if (exchanges != null) { + urlBuilder.addQueryParameter("exchanges", exchanges.stream() + .map(Exchange::value) + .collect(Collectors.joining(","))); + } + + Request request = alpacaClient.requestBuilder(urlBuilder.build()) + .get() + .build(); + return alpacaClient.requestObject(request, XbboResponse.class); + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/MarketDataEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/StockMarketDataEndpoint.java similarity index 86% rename from src/main/java/net/jacobpeterson/alpaca/rest/endpoint/MarketDataEndpoint.java rename to src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/StockMarketDataEndpoint.java index 45fc4a08..f203a5d2 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/MarketDataEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/StockMarketDataEndpoint.java @@ -1,19 +1,20 @@ -package net.jacobpeterson.alpaca.rest.endpoint; +package net.jacobpeterson.alpaca.rest.endpoint.marketdata; import com.google.gson.reflect.TypeToken; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.bar.Bar; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.bar.BarsResponse; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.bar.enums.BarAdjustment; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.bar.enums.BarTimePeriod; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.quote.LatestQuoteResponse; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.quote.Quote; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.quote.QuotesResponse; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.snapshot.Snapshot; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.trade.LatestTradeResponse; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.trade.Trade; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.historical.trade.TradesResponse; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.enums.BarTimePeriod; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.Bar; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.BarsResponse; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.enums.BarAdjustment; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.LatestQuoteResponse; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.Quote; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.QuotesResponse; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.snapshot.Snapshot; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.LatestTradeResponse; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.Trade; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.TradesResponse; import net.jacobpeterson.alpaca.rest.AlpacaClient; import net.jacobpeterson.alpaca.rest.AlpacaClientException; +import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; import net.jacobpeterson.alpaca.util.format.FormatUtil; import okhttp3.HttpUrl; import okhttp3.Request; @@ -31,14 +32,14 @@ * Historical * Market Data API v2. */ -public class MarketDataEndpoint extends AlpacaEndpoint { +public class StockMarketDataEndpoint extends AlpacaEndpoint { /** - * Instantiates a new {@link MarketDataEndpoint}. + * Instantiates a new {@link StockMarketDataEndpoint}. * * @param alpacaClient the {@link AlpacaClient} */ - public MarketDataEndpoint(AlpacaClient alpacaClient) { + public StockMarketDataEndpoint(AlpacaClient alpacaClient) { super(alpacaClient, "stocks"); } @@ -87,7 +88,7 @@ public TradesResponse getTrades(String symbol, ZonedDateTime start, ZonedDateTim } /** - * The Latest Trade API provides the latest {@link Trade} data for a given ticker symbol. + * Gets the latest {@link Trade} for the requested security. * * @param symbol the symbol to query for * @@ -155,7 +156,7 @@ public QuotesResponse getQuotes(String symbol, ZonedDateTime start, ZonedDateTim } /** - * The Latest Quote API provides the latest quote data for a given ticker symbol. + * Gets the latest {@link Quote} for the requested security. * * @param symbol the symbol to query for * @@ -217,7 +218,6 @@ public BarsResponse getBars(String symbol, ZonedDateTime start, ZonedDateTime en urlBuilder.addQueryParameter("start", FormatUtil.toRFC3339Format(start)); urlBuilder.addQueryParameter("end", FormatUtil.toRFC3339Format(end)); - urlBuilder.addQueryParameter("timeframe", barTimePeriodDuration + barTimePeriod.toString()); if (limit != null) { urlBuilder.addQueryParameter("limit", limit.toString()); @@ -227,6 +227,8 @@ public BarsResponse getBars(String symbol, ZonedDateTime start, ZonedDateTime en urlBuilder.addQueryParameter("page_token", pageToken); } + urlBuilder.addQueryParameter("timeframe", barTimePeriodDuration + barTimePeriod.toString()); + if (barAdjustment != null) { urlBuilder.addQueryParameter("adjustment", barAdjustment.toString()); } @@ -238,8 +240,7 @@ public BarsResponse getBars(String symbol, ZonedDateTime start, ZonedDateTime en } /** - * The Snapshot API for one ticker provides the latest trade, latest quote, minute bar daily bar, and previous daily - * bar data for a given ticker symbol + * Gets {@link Snapshot}s of the requested securities. * * @param symbols a {@link Collection} of symbols to query for * @@ -264,8 +265,7 @@ public Map getSnapshots(Collection symbols) throws Alp } /** - * The Snapshot API for one ticker provides the latest trade, latest quote, minute bar daily bar and previous daily - * bar data for a given ticker symbol + * Gets a {@link Snapshot} of the requested security. * * @param symbol the symbol to query for * diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java index 232d23f2..18387bcd 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java @@ -275,38 +275,18 @@ public void setListener(L listener) { this.listener = listener; } - /** - * Gets {@link #websocketStateListener}. - * - * @return the {@link WebsocketStateListener} - */ public WebsocketStateListener getWebsocketStateListener() { return websocketStateListener; } - /** - * Sets {@link #websocketStateListener}. - * - * @param websocketStateListener an {@link WebsocketStateListener} - */ public void setWebsocketStateListener(WebsocketStateListener websocketStateListener) { this.websocketStateListener = websocketStateListener; } - /** - * Gets {@link #automaticallyReconnect}. - * - * @return a boolean - */ public boolean doesAutomaticallyReconnect() { return automaticallyReconnect; } - /** - * Sets {@link #automaticallyReconnect}. - * - * @param automaticallyReconnect the boolean - */ public void setAutomaticallyReconnect(boolean automaticallyReconnect) { this.automaticallyReconnect = automaticallyReconnect; } diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataListener.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataListener.java index c1138209..09bf4266 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataListener.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataListener.java @@ -1,7 +1,7 @@ package net.jacobpeterson.alpaca.websocket.marketdata; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.realtime.MarketDataMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.realtime.enums.MarketDataMessageType; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.MarketDataMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.enums.MarketDataMessageType; import net.jacobpeterson.alpaca.websocket.AlpacaWebsocketMessageListener; /** diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java index 48953560..28cce551 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java @@ -5,14 +5,14 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.realtime.MarketDataMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.realtime.bar.BarMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.realtime.control.ErrorMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.realtime.control.SubscriptionsMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.realtime.control.SuccessMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.realtime.enums.MarketDataMessageType; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.realtime.quote.QuoteMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.realtime.trade.TradeMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.MarketDataMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.bar.BarMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.control.ErrorMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.control.SubscriptionsMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.control.SuccessMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.enums.MarketDataMessageType; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.quote.QuoteMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.trade.TradeMessage; import net.jacobpeterson.alpaca.model.properties.DataAPIType; import net.jacobpeterson.alpaca.websocket.AlpacaWebsocket; import okhttp3.HttpUrl; diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocketInterface.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocketInterface.java index 9ac90112..2eb52e63 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocketInterface.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocketInterface.java @@ -1,10 +1,10 @@ package net.jacobpeterson.alpaca.websocket.marketdata; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.realtime.MarketDataMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.realtime.control.ErrorMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.realtime.control.SubscriptionsMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.realtime.control.SuccessMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.realtime.enums.MarketDataMessageType; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.MarketDataMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.control.ErrorMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.control.SubscriptionsMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.control.SuccessMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.enums.MarketDataMessageType; import net.jacobpeterson.alpaca.websocket.AlpacaWebsocketInterface; import java.util.Collection; From 95b36855c9a95e783f37196984da04d7b56880a9 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Tue, 21 Dec 2021 18:21:33 -0700 Subject: [PATCH 23/84] Refactor, abstract models and endpoints, implement realtime crypto --- README.md | 2 +- schema_json/endpoint/account/account.json | 4 +- .../endpoint/{asset => assets}/asset.json | 8 +-- .../{asset => assets}/enums/asset_class.json | 0 .../{asset => assets}/enums/asset_status.json | 0 .../{stock => common}/historical/bar/bar.json | 10 ++- .../bar}/enums/bar_time_period.json | 0 .../common/historical/quote/quote.json | 21 ++++++ .../common/historical/trade/trade.json | 21 ++++++ .../realtime/bar/bar_message.json | 9 +-- .../realtime/control/error_message.json | 4 +- .../control/subscriptions_message.json | 4 +- .../realtime/control/success_message.json | 4 +- .../enums/market_data_message_type.json | 2 +- .../realtime/market_data_message.json | 6 +- .../common/realtime/quote/quote_message.json | 24 +++++++ .../realtime/symbol_message.json | 4 +- .../common/realtime/trade/trade_message.json | 24 +++++++ .../enums/exchange.json | 2 +- .../trade => common}/enums/taker_side.json | 2 +- .../crypto/historical/bar/bar.json | 51 -------------- .../crypto/historical/bar/crypto_bar.json | 19 +++++ ...esponse.json => crypto_bars_response.json} | 6 +- .../crypto/historical/quote/crypto_quote.json | 24 +++++++ ...ponse.json => crypto_quotes_response.json} | 6 +- ...json => latest_crypto_quote_response.json} | 6 +- .../crypto/historical/quote/quote.json | 36 ---------- .../crypto/historical/trade/crypto_trade.json | 24 +++++++ ...ponse.json => crypto_trades_response.json} | 6 +- ...json => latest_crypto_trade_response.json} | 6 +- .../crypto/historical/trade/trade.json | 36 ---------- .../crypto/historical/xbbo/xbbo.json | 6 +- .../crypto/historical/xbbo/xbbo_response.json | 2 +- .../realtime/bar/crypto_bar_message.json | 29 ++++++++ .../realtime/quote/crypto_quote_message.json | 24 +++++++ .../realtime/trade/crypto_trade_message.json | 24 +++++++ .../historical/bar/enums/bar_adjustment.json | 2 +- .../stock/historical/bar/stock_bar.json | 14 ++++ ...response.json => stock_bars_response.json} | 6 +- ....json => latest_stock_quote_response.json} | 6 +- .../quote/{quote.json => stock_quote.json} | 20 ++---- ...sponse.json => stock_quotes_response.json} | 6 +- .../stock/historical/snapshot/snapshot.json | 22 +++--- ....json => latest_stock_trade_response.json} | 6 +- .../trade/{trade.json => stock_trade.json} | 20 ++---- ...sponse.json => stock_trades_response.json} | 6 +- .../stock/realtime/bar/stock_bar_message.json | 14 ++++ ..._message.json => stock_quote_message.json} | 19 +---- ..._message.json => stock_trade_message.json} | 19 +---- .../{order => orders}/cancelled_order.json | 4 +- .../enums/current_order_status.json | 0 .../{order => orders}/enums/order_class.json | 0 .../{order => orders}/enums/order_side.json | 0 .../{order => orders}/enums/order_status.json | 0 .../enums/order_time_in_force.json | 0 .../{order => orders}/enums/order_type.json | 0 .../endpoint/{order => orders}/order.json | 24 +++---- .../close_position_order.json | 4 +- .../{position => positions}/position.json | 0 .../streaming/trade/trade_update.json | 4 +- schema_json/endpoint/watchlist/watchlist.json | 2 +- .../net/jacobpeterson/alpaca/AlpacaAPI.java | 50 ++++++++----- .../{ => account}/AccountEndpoint.java | 3 +- .../AccountActivitiesEndpoint.java | 3 +- .../AccountConfigurationEndpoint.java | 3 +- .../endpoint/{ => assets}/AssetsEndpoint.java | 9 +-- .../{ => calendar}/CalendarEndpoint.java | 3 +- .../endpoint/{ => clock}/ClockEndpoint.java | 3 +- .../CryptoMarketDataEndpoint.java | 70 +++++++++---------- .../{ => stock}/StockMarketDataEndpoint.java | 60 ++++++++-------- .../endpoint/{ => orders}/OrdersEndpoint.java | 19 ++--- .../PortfolioHistoryEndpoint.java | 3 +- .../{ => positions}/PositionsEndpoint.java | 11 +-- .../{ => watchlist}/WatchlistEndpoint.java | 5 +- .../marketdata/MarketDataListener.java | 4 +- .../marketdata/MarketDataWebsocket.java | 70 ++++++++++++------- .../MarketDataWebsocketInterface.java | 10 +-- .../crypto/CryptoMarketDataWebsocket.java | 27 +++++++ .../stock/StockMarketDataWebsocket.java | 30 ++++++++ .../alpaca/test/live/AlpacaAPITest.java | 14 ++-- 80 files changed, 612 insertions(+), 439 deletions(-) rename schema_json/endpoint/{asset => assets}/asset.json (93%) rename schema_json/endpoint/{asset => assets}/enums/asset_class.json (100%) rename schema_json/endpoint/{asset => assets}/enums/asset_status.json (100%) rename schema_json/endpoint/market_data/{stock => common}/historical/bar/bar.json (83%) rename schema_json/endpoint/market_data/common/{ => historical/bar}/enums/bar_time_period.json (100%) create mode 100644 schema_json/endpoint/market_data/common/historical/quote/quote.json create mode 100644 schema_json/endpoint/market_data/common/historical/trade/trade.json rename schema_json/endpoint/market_data/{stock => common}/realtime/bar/bar_message.json (77%) rename schema_json/endpoint/market_data/{stock => common}/realtime/control/error_message.json (76%) rename schema_json/endpoint/market_data/{stock => common}/realtime/control/subscriptions_message.json (84%) rename schema_json/endpoint/market_data/{stock => common}/realtime/control/success_message.json (71%) rename schema_json/endpoint/market_data/{stock => common}/realtime/enums/market_data_message_type.json (83%) rename schema_json/endpoint/market_data/{stock => common}/realtime/market_data_message.json (59%) create mode 100644 schema_json/endpoint/market_data/common/realtime/quote/quote_message.json rename schema_json/endpoint/market_data/{stock => common}/realtime/symbol_message.json (71%) create mode 100644 schema_json/endpoint/market_data/common/realtime/trade/trade_message.json rename schema_json/endpoint/market_data/crypto/{historical => common}/enums/exchange.json (74%) rename schema_json/endpoint/market_data/crypto/{historical/trade => common}/enums/taker_side.json (69%) delete mode 100644 schema_json/endpoint/market_data/crypto/historical/bar/bar.json create mode 100644 schema_json/endpoint/market_data/crypto/historical/bar/crypto_bar.json rename schema_json/endpoint/market_data/crypto/historical/bar/{bars_response.json => crypto_bars_response.json} (76%) create mode 100644 schema_json/endpoint/market_data/crypto/historical/quote/crypto_quote.json rename schema_json/endpoint/market_data/crypto/historical/quote/{quotes_response.json => crypto_quotes_response.json} (75%) rename schema_json/endpoint/market_data/crypto/historical/quote/{latest_quote_response.json => latest_crypto_quote_response.json} (64%) delete mode 100644 schema_json/endpoint/market_data/crypto/historical/quote/quote.json create mode 100644 schema_json/endpoint/market_data/crypto/historical/trade/crypto_trade.json rename schema_json/endpoint/market_data/crypto/historical/trade/{trades_response.json => crypto_trades_response.json} (75%) rename schema_json/endpoint/market_data/crypto/historical/trade/{latest_trade_response.json => latest_crypto_trade_response.json} (64%) delete mode 100644 schema_json/endpoint/market_data/crypto/historical/trade/trade.json create mode 100644 schema_json/endpoint/market_data/crypto/realtime/bar/crypto_bar_message.json create mode 100644 schema_json/endpoint/market_data/crypto/realtime/quote/crypto_quote_message.json create mode 100644 schema_json/endpoint/market_data/crypto/realtime/trade/crypto_trade_message.json create mode 100644 schema_json/endpoint/market_data/stock/historical/bar/stock_bar.json rename schema_json/endpoint/market_data/stock/historical/bar/{bars_response.json => stock_bars_response.json} (85%) rename schema_json/endpoint/market_data/stock/historical/quote/{latest_quote_response.json => latest_stock_quote_response.json} (76%) rename schema_json/endpoint/market_data/stock/historical/quote/{quote.json => stock_quote.json} (66%) rename schema_json/endpoint/market_data/stock/historical/quote/{quotes_response.json => stock_quotes_response.json} (84%) rename schema_json/endpoint/market_data/stock/historical/trade/{latest_trade_response.json => latest_stock_trade_response.json} (76%) rename schema_json/endpoint/market_data/stock/historical/trade/{trade.json => stock_trade.json} (63%) rename schema_json/endpoint/market_data/stock/historical/trade/{trades_response.json => stock_trades_response.json} (84%) create mode 100644 schema_json/endpoint/market_data/stock/realtime/bar/stock_bar_message.json rename schema_json/endpoint/market_data/stock/realtime/quote/{quote_message.json => stock_quote_message.json} (65%) rename schema_json/endpoint/market_data/stock/realtime/trade/{trade_message.json => stock_trade_message.json} (59%) rename schema_json/endpoint/{order => orders}/cancelled_order.json (92%) rename schema_json/endpoint/{order => orders}/enums/current_order_status.json (100%) rename schema_json/endpoint/{order => orders}/enums/order_class.json (100%) rename schema_json/endpoint/{order => orders}/enums/order_side.json (100%) rename schema_json/endpoint/{order => orders}/enums/order_status.json (100%) rename schema_json/endpoint/{order => orders}/enums/order_time_in_force.json (100%) rename schema_json/endpoint/{order => orders}/enums/order_type.json (100%) rename schema_json/endpoint/{order => orders}/order.json (89%) rename schema_json/endpoint/{position => positions}/close_position_order.json (91%) rename schema_json/endpoint/{position => positions}/position.json (100%) rename src/main/java/net/jacobpeterson/alpaca/rest/endpoint/{ => account}/AccountEndpoint.java (90%) rename src/main/java/net/jacobpeterson/alpaca/rest/endpoint/{ => accountactivities}/AccountActivitiesEndpoint.java (97%) rename src/main/java/net/jacobpeterson/alpaca/rest/endpoint/{ => accountconfiguration}/AccountConfigurationEndpoint.java (94%) rename src/main/java/net/jacobpeterson/alpaca/rest/endpoint/{ => assets}/AssetsEndpoint.java (88%) rename src/main/java/net/jacobpeterson/alpaca/rest/endpoint/{ => calendar}/CalendarEndpoint.java (94%) rename src/main/java/net/jacobpeterson/alpaca/rest/endpoint/{ => clock}/ClockEndpoint.java (91%) rename src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/{ => crypto}/CryptoMarketDataEndpoint.java (83%) rename src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/{ => stock}/StockMarketDataEndpoint.java (84%) rename src/main/java/net/jacobpeterson/alpaca/rest/endpoint/{ => orders}/OrdersEndpoint.java (98%) rename src/main/java/net/jacobpeterson/alpaca/rest/endpoint/{ => portfoliohistory}/PortfolioHistoryEndpoint.java (97%) rename src/main/java/net/jacobpeterson/alpaca/rest/endpoint/{ => positions}/PositionsEndpoint.java (92%) rename src/main/java/net/jacobpeterson/alpaca/rest/endpoint/{ => watchlist}/WatchlistEndpoint.java (97%) create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/crypto/CryptoMarketDataWebsocket.java create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/stock/StockMarketDataWebsocket.java diff --git a/README.md b/README.md index a0947ec0..ffbea854 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ try { ``` ## [`CryptoMarketDataEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/CryptoMarketDataEndpoint.java) -Alpaca provides cryptocurrency data from multiple venues/exchanges, namely: Coinbase, ErisX, and FTX. Note that it does not route orders to all venues. +Alpaca provides cryptocurrency data from multiple venues/exchanges, namely: Coinbase, ErisX, and FTX. Example usage: ```java diff --git a/schema_json/endpoint/account/account.json b/schema_json/endpoint/account/account.json index a6e6d506..7e558fe2 100644 --- a/schema_json/endpoint/account/account.json +++ b/schema_json/endpoint/account/account.json @@ -56,11 +56,11 @@ }, "long_market_value": { "existingJavaType": "java.lang.String", - "title": "Real-time MtM value of all long positions held in the account." + "title": "Realtime MtM value of all long positions held in the account." }, "short_market_value": { "existingJavaType": "java.lang.String", - "title": "Real-time MtM value of all short positions held in the account." + "title": "Realtime MtM value of all short positions held in the account." }, "equity": { "existingJavaType": "java.lang.String", diff --git a/schema_json/endpoint/asset/asset.json b/schema_json/endpoint/assets/asset.json similarity index 93% rename from schema_json/endpoint/asset/asset.json rename to schema_json/endpoint/assets/asset.json index f5ab8b9c..fa65e096 100644 --- a/schema_json/endpoint/asset/asset.json +++ b/schema_json/endpoint/assets/asset.json @@ -7,9 +7,9 @@ "title": "Asset ID." }, "class": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.asset.enums.AssetClass", + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.assets.enums.AssetClass", "javaName": "assetClass", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.asset.enums.AssetClass}." + "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.assets.enums.AssetClass}." }, "exchange": { "existingJavaType": "java.lang.String", @@ -24,8 +24,8 @@ "title": "The official name of the asset." }, "status": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.asset.enums.AssetStatus", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.asset.enums.AssetStatus}." + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.assets.enums.AssetStatus", + "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.assets.enums.AssetStatus}." }, "tradable": { "existingJavaType": "java.lang.Boolean", diff --git a/schema_json/endpoint/asset/enums/asset_class.json b/schema_json/endpoint/assets/enums/asset_class.json similarity index 100% rename from schema_json/endpoint/asset/enums/asset_class.json rename to schema_json/endpoint/assets/enums/asset_class.json diff --git a/schema_json/endpoint/asset/enums/asset_status.json b/schema_json/endpoint/assets/enums/asset_status.json similarity index 100% rename from schema_json/endpoint/asset/enums/asset_status.json rename to schema_json/endpoint/assets/enums/asset_status.json diff --git a/schema_json/endpoint/market_data/stock/historical/bar/bar.json b/schema_json/endpoint/market_data/common/historical/bar/bar.json similarity index 83% rename from schema_json/endpoint/market_data/stock/historical/bar/bar.json rename to schema_json/endpoint/market_data/common/historical/bar/bar.json index 55423fde..b0284b57 100644 --- a/schema_json/endpoint/market_data/stock/historical/bar/bar.json +++ b/schema_json/endpoint/market_data/common/historical/bar/bar.json @@ -1,6 +1,9 @@ { "type": "object", - "title": "See Historical Data.", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.quote.QuoteMessage" + }, + "title": "See Market Data.", "properties": { "t": { "existingJavaType": "java.time.ZonedDateTime", @@ -27,11 +30,6 @@ "javaName": "close", "title": "Close price." }, - "v": { - "existingJavaType": "java.lang.Long", - "javaName": "volume", - "title": "Volume." - }, "n": { "existingJavaType": "java.lang.Long", "javaName": "tradeCount", diff --git a/schema_json/endpoint/market_data/common/enums/bar_time_period.json b/schema_json/endpoint/market_data/common/historical/bar/enums/bar_time_period.json similarity index 100% rename from schema_json/endpoint/market_data/common/enums/bar_time_period.json rename to schema_json/endpoint/market_data/common/historical/bar/enums/bar_time_period.json diff --git a/schema_json/endpoint/market_data/common/historical/quote/quote.json b/schema_json/endpoint/market_data/common/historical/quote/quote.json new file mode 100644 index 00000000..ed75e88f --- /dev/null +++ b/schema_json/endpoint/market_data/common/historical/quote/quote.json @@ -0,0 +1,21 @@ +{ + "type": "object", + "title": "See Market Data.", + "properties": { + "t": { + "existingJavaType": "java.time.ZonedDateTime", + "javaName": "timestamp", + "title": "Timestamp with nanosecond precision." + }, + "ap": { + "existingJavaType": "java.lang.Double", + "javaName": "askPrice", + "title": "Ask price." + }, + "bp": { + "existingJavaType": "java.lang.Double", + "javaName": "bidPrice", + "title": "Bid price." + } + } +} diff --git a/schema_json/endpoint/market_data/common/historical/trade/trade.json b/schema_json/endpoint/market_data/common/historical/trade/trade.json new file mode 100644 index 00000000..1cacafa5 --- /dev/null +++ b/schema_json/endpoint/market_data/common/historical/trade/trade.json @@ -0,0 +1,21 @@ +{ + "type": "object", + "title": "See Market Data.", + "properties": { + "t": { + "existingJavaType": "java.time.ZonedDateTime", + "javaName": "timestamp", + "title": "Timestamp with nanosecond precision." + }, + "p": { + "existingJavaType": "java.lang.Double", + "javaName": "price", + "title": "Trade price." + }, + "i": { + "existingJavaType": "java.lang.Long", + "javaName": "tradeID", + "title": "Trade ID." + } + } +} diff --git a/schema_json/endpoint/market_data/stock/realtime/bar/bar_message.json b/schema_json/endpoint/market_data/common/realtime/bar/bar_message.json similarity index 77% rename from schema_json/endpoint/market_data/stock/realtime/bar/bar_message.json rename to schema_json/endpoint/market_data/common/realtime/bar/bar_message.json index 7abf464d..fcf5f6c6 100644 --- a/schema_json/endpoint/market_data/stock/realtime/bar/bar_message.json +++ b/schema_json/endpoint/market_data/common/realtime/bar/bar_message.json @@ -1,9 +1,9 @@ { "type": "object", "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.SymbolMessage" + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.SymbolMessage" }, - "title": "See Real-time Data.", + "title": "See Market Data.", "properties": { "o": { "existingJavaType": "java.lang.Double", @@ -25,11 +25,6 @@ "javaName": "close", "title": "Close price." }, - "v": { - "existingJavaType": "java.lang.Long", - "javaName": "volume", - "title": "Volume." - }, "t": { "existingJavaType": "java.time.ZonedDateTime", "javaName": "timestamp", diff --git a/schema_json/endpoint/market_data/stock/realtime/control/error_message.json b/schema_json/endpoint/market_data/common/realtime/control/error_message.json similarity index 76% rename from schema_json/endpoint/market_data/stock/realtime/control/error_message.json rename to schema_json/endpoint/market_data/common/realtime/control/error_message.json index 1ef275ab..39b9e7f2 100644 --- a/schema_json/endpoint/market_data/stock/realtime/control/error_message.json +++ b/schema_json/endpoint/market_data/common/realtime/control/error_message.json @@ -1,9 +1,9 @@ { "type": "object", "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.MarketDataMessage" + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.MarketDataMessage" }, - "title": "See Real-time Data.", + "title": "See Market Data.", "properties": { "code": { "existingJavaType": "java.lang.Integer", diff --git a/schema_json/endpoint/market_data/stock/realtime/control/subscriptions_message.json b/schema_json/endpoint/market_data/common/realtime/control/subscriptions_message.json similarity index 84% rename from schema_json/endpoint/market_data/stock/realtime/control/subscriptions_message.json rename to schema_json/endpoint/market_data/common/realtime/control/subscriptions_message.json index 76bfc479..c302083a 100644 --- a/schema_json/endpoint/market_data/stock/realtime/control/subscriptions_message.json +++ b/schema_json/endpoint/market_data/common/realtime/control/subscriptions_message.json @@ -1,9 +1,9 @@ { "type": "object", "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.MarketDataMessage" + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.MarketDataMessage" }, - "title": "See Real-time Data.", + "title": "See Market Data.", "properties": { "trades": { "existingJavaType": "java.util.ArrayList", diff --git a/schema_json/endpoint/market_data/stock/realtime/control/success_message.json b/schema_json/endpoint/market_data/common/realtime/control/success_message.json similarity index 71% rename from schema_json/endpoint/market_data/stock/realtime/control/success_message.json rename to schema_json/endpoint/market_data/common/realtime/control/success_message.json index d7225e02..2edd335b 100644 --- a/schema_json/endpoint/market_data/stock/realtime/control/success_message.json +++ b/schema_json/endpoint/market_data/common/realtime/control/success_message.json @@ -1,9 +1,9 @@ { "type": "object", "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.MarketDataMessage" + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.MarketDataMessage" }, - "title": "See Real-time Data.", + "title": "See Market Data.", "properties": { "msg": { "existingJavaType": "java.lang.String", diff --git a/schema_json/endpoint/market_data/stock/realtime/enums/market_data_message_type.json b/schema_json/endpoint/market_data/common/realtime/enums/market_data_message_type.json similarity index 83% rename from schema_json/endpoint/market_data/stock/realtime/enums/market_data_message_type.json rename to schema_json/endpoint/market_data/common/realtime/enums/market_data_message_type.json index 40368ec9..f55a5190 100644 --- a/schema_json/endpoint/market_data/stock/realtime/enums/market_data_message_type.json +++ b/schema_json/endpoint/market_data/common/realtime/enums/market_data_message_type.json @@ -1,6 +1,6 @@ { "type": "string", - "title": "See Real-time Data.", + "title": "See Market Data.", "enum": [ "success", "error", diff --git a/schema_json/endpoint/market_data/stock/realtime/market_data_message.json b/schema_json/endpoint/market_data/common/realtime/market_data_message.json similarity index 59% rename from schema_json/endpoint/market_data/stock/realtime/market_data_message.json rename to schema_json/endpoint/market_data/common/realtime/market_data_message.json index 5ee64bef..d41cd481 100644 --- a/schema_json/endpoint/market_data/stock/realtime/market_data_message.json +++ b/schema_json/endpoint/market_data/common/realtime/market_data_message.json @@ -1,11 +1,11 @@ { "type": "object", - "title": "See Real-time Data.", + "title": "See Market Data.", "properties": { "T": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.enums.MarketDataMessageType", + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.enums.MarketDataMessageType", "javaName": "messageType", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.enums.MarketDataMessageType}." + "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.enums.MarketDataMessageType}." } } } diff --git a/schema_json/endpoint/market_data/common/realtime/quote/quote_message.json b/schema_json/endpoint/market_data/common/realtime/quote/quote_message.json new file mode 100644 index 00000000..d6bdaabc --- /dev/null +++ b/schema_json/endpoint/market_data/common/realtime/quote/quote_message.json @@ -0,0 +1,24 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.SymbolMessage" + }, + "title": "See Market Data.", + "properties": { + "ap": { + "existingJavaType": "java.lang.Double", + "javaName": "askPrice", + "title": "Ask price." + }, + "bp": { + "existingJavaType": "java.lang.Double", + "javaName": "bidPrice", + "title": "Bid price." + }, + "t": { + "existingJavaType": "java.time.ZonedDateTime", + "javaName": "timestamp", + "title": "Timestamp with nanosecond precision." + } + } +} diff --git a/schema_json/endpoint/market_data/stock/realtime/symbol_message.json b/schema_json/endpoint/market_data/common/realtime/symbol_message.json similarity index 71% rename from schema_json/endpoint/market_data/stock/realtime/symbol_message.json rename to schema_json/endpoint/market_data/common/realtime/symbol_message.json index acc6327c..a6f40f69 100644 --- a/schema_json/endpoint/market_data/stock/realtime/symbol_message.json +++ b/schema_json/endpoint/market_data/common/realtime/symbol_message.json @@ -1,9 +1,9 @@ { "type": "object", "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.MarketDataMessage" + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.MarketDataMessage" }, - "title": "See Real-time Data.", + "title": "See Market Data.", "properties": { "S": { "existingJavaType": "java.lang.String", diff --git a/schema_json/endpoint/market_data/common/realtime/trade/trade_message.json b/schema_json/endpoint/market_data/common/realtime/trade/trade_message.json new file mode 100644 index 00000000..ca01450f --- /dev/null +++ b/schema_json/endpoint/market_data/common/realtime/trade/trade_message.json @@ -0,0 +1,24 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.SymbolMessage" + }, + "title": "See Market Data.", + "properties": { + "i": { + "existingJavaType": "java.lang.Integer", + "javaName": "tradeID", + "title": "Trade ID." + }, + "p": { + "existingJavaType": "java.lang.Double", + "javaName": "price", + "title": "Trade price." + }, + "t": { + "existingJavaType": "java.time.ZonedDateTime", + "javaName": "timestamp", + "title": "Timestamp with nanosecond precision." + } + } +} diff --git a/schema_json/endpoint/market_data/crypto/historical/enums/exchange.json b/schema_json/endpoint/market_data/crypto/common/enums/exchange.json similarity index 74% rename from schema_json/endpoint/market_data/crypto/historical/enums/exchange.json rename to schema_json/endpoint/market_data/crypto/common/enums/exchange.json index 1ea41bd6..ff9d94e5 100644 --- a/schema_json/endpoint/market_data/crypto/historical/enums/exchange.json +++ b/schema_json/endpoint/market_data/crypto/common/enums/exchange.json @@ -1,6 +1,6 @@ { "type": "string", - "title": "See Crypto Historical Data.", + "title": "See Crypto Data.", "enum": [ "ERSX", "FTX", diff --git a/schema_json/endpoint/market_data/crypto/historical/trade/enums/taker_side.json b/schema_json/endpoint/market_data/crypto/common/enums/taker_side.json similarity index 69% rename from schema_json/endpoint/market_data/crypto/historical/trade/enums/taker_side.json rename to schema_json/endpoint/market_data/crypto/common/enums/taker_side.json index 9604bc54..8c8e4adf 100644 --- a/schema_json/endpoint/market_data/crypto/historical/trade/enums/taker_side.json +++ b/schema_json/endpoint/market_data/crypto/common/enums/taker_side.json @@ -1,6 +1,6 @@ { "type": "string", - "title": "See Crypto Historical Data.", + "title": "See Crypto Data.", "enum": [ "B", "S" diff --git a/schema_json/endpoint/market_data/crypto/historical/bar/bar.json b/schema_json/endpoint/market_data/crypto/historical/bar/bar.json deleted file mode 100644 index c677ba92..00000000 --- a/schema_json/endpoint/market_data/crypto/historical/bar/bar.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "type": "object", - "title": "See Crypto Historical Data.", - "properties": { - "t": { - "existingJavaType": "java.time.ZonedDateTime", - "javaName": "timestamp", - "title": "Timestamp with nanosecond precision." - }, - "x": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.enums.Exchange", - "javaName": "exchange", - "title": "Exchange." - }, - "o": { - "existingJavaType": "java.lang.Double", - "javaName": "open", - "title": "Open price." - }, - "h": { - "existingJavaType": "java.lang.Double", - "javaName": "high", - "title": "High price." - }, - "l": { - "existingJavaType": "java.lang.Double", - "javaName": "low", - "title": "Low price." - }, - "c": { - "existingJavaType": "java.lang.Double", - "javaName": "close", - "title": "Close price." - }, - "v": { - "existingJavaType": "java.lang.Double", - "javaName": "volume", - "title": "Volume." - }, - "n": { - "existingJavaType": "java.lang.Long", - "javaName": "tradeCount", - "title": "Trade count." - }, - "vw": { - "existingJavaType": "java.lang.Double", - "javaName": "vwap", - "title": "VWAP (Volume Weighted Average Price)." - } - } -} diff --git a/schema_json/endpoint/market_data/crypto/historical/bar/crypto_bar.json b/schema_json/endpoint/market_data/crypto/historical/bar/crypto_bar.json new file mode 100644 index 00000000..e23c3ec6 --- /dev/null +++ b/schema_json/endpoint/market_data/crypto/historical/bar/crypto_bar.json @@ -0,0 +1,19 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.historical.bar.Bar" + }, + "title": "See Historical Crypto Data.", + "properties": { + "x": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.common.enums.Exchange", + "javaName": "exchange", + "title": "Exchange." + }, + "v": { + "existingJavaType": "java.lang.Double", + "javaName": "volume", + "title": "Volume." + } + } +} diff --git a/schema_json/endpoint/market_data/crypto/historical/bar/bars_response.json b/schema_json/endpoint/market_data/crypto/historical/bar/crypto_bars_response.json similarity index 76% rename from schema_json/endpoint/market_data/crypto/historical/bar/bars_response.json rename to schema_json/endpoint/market_data/crypto/historical/bar/crypto_bars_response.json index 21f5cddb..677e6633 100644 --- a/schema_json/endpoint/market_data/crypto/historical/bar/bars_response.json +++ b/schema_json/endpoint/market_data/crypto/historical/bar/crypto_bars_response.json @@ -1,10 +1,10 @@ { "type": "object", - "title": "See Historical Data.", + "title": "See Historical Crypto Data.", "properties": { "bars": { - "existingJavaType": "java.util.ArrayList", - "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.bar.Bar}s." + "existingJavaType": "java.util.ArrayList", + "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.bar.CryptoBar}s." }, "symbol": { "existingJavaType": "java.lang.String", diff --git a/schema_json/endpoint/market_data/crypto/historical/quote/crypto_quote.json b/schema_json/endpoint/market_data/crypto/historical/quote/crypto_quote.json new file mode 100644 index 00000000..099df001 --- /dev/null +++ b/schema_json/endpoint/market_data/crypto/historical/quote/crypto_quote.json @@ -0,0 +1,24 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.historical.quote.Quote" + }, + "title": "See Historical Crypto Data.", + "properties": { + "x": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.common.enums.Exchange", + "javaName": "exchange", + "title": "Exchange." + }, + "as": { + "existingJavaType": "java.lang.Double", + "javaName": "askSize", + "title": "Ask size." + }, + "bs": { + "existingJavaType": "java.lang.Double", + "javaName": "bidSize", + "title": "Bid size." + } + } +} diff --git a/schema_json/endpoint/market_data/crypto/historical/quote/quotes_response.json b/schema_json/endpoint/market_data/crypto/historical/quote/crypto_quotes_response.json similarity index 75% rename from schema_json/endpoint/market_data/crypto/historical/quote/quotes_response.json rename to schema_json/endpoint/market_data/crypto/historical/quote/crypto_quotes_response.json index 212635c3..e8a67bfd 100644 --- a/schema_json/endpoint/market_data/crypto/historical/quote/quotes_response.json +++ b/schema_json/endpoint/market_data/crypto/historical/quote/crypto_quotes_response.json @@ -1,10 +1,10 @@ { "type": "object", - "title": "See Crypto Historical Data.", + "title": "See Historical Crypto Data.", "properties": { "quotes": { - "existingJavaType": "java.util.ArrayList", - "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.Quote}s." + "existingJavaType": "java.util.ArrayList", + "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.CryptoQuote}s." }, "symbol": { "existingJavaType": "java.lang.String", diff --git a/schema_json/endpoint/market_data/crypto/historical/quote/latest_quote_response.json b/schema_json/endpoint/market_data/crypto/historical/quote/latest_crypto_quote_response.json similarity index 64% rename from schema_json/endpoint/market_data/crypto/historical/quote/latest_quote_response.json rename to schema_json/endpoint/market_data/crypto/historical/quote/latest_crypto_quote_response.json index 044d252f..64bf1d75 100644 --- a/schema_json/endpoint/market_data/crypto/historical/quote/latest_quote_response.json +++ b/schema_json/endpoint/market_data/crypto/historical/quote/latest_crypto_quote_response.json @@ -1,14 +1,14 @@ { "type": "object", - "title": "See Crypto Historical Data.", + "title": "See Historical Crypto Data.", "properties": { "symbol": { "existingJavaType": "java.lang.String", "title": "Symbol that was queried." }, "quote": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.Quote", - "title": "The latest {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.Quote}." + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.CryptoQuote", + "title": "The latest {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.CryptoQuote}." } } } diff --git a/schema_json/endpoint/market_data/crypto/historical/quote/quote.json b/schema_json/endpoint/market_data/crypto/historical/quote/quote.json deleted file mode 100644 index ecca5a55..00000000 --- a/schema_json/endpoint/market_data/crypto/historical/quote/quote.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "type": "object", - "title": "See Crypto Historical Data.", - "properties": { - "t": { - "existingJavaType": "java.time.ZonedDateTime", - "javaName": "timestamp", - "title": "Timestamp with nanosecond precision." - }, - "x": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.enums.Exchange", - "javaName": "exchange", - "title": "Exchange." - }, - "ap": { - "existingJavaType": "java.lang.Double", - "javaName": "askPrice", - "title": "Ask price." - }, - "as": { - "existingJavaType": "java.lang.Double", - "javaName": "askSize", - "title": "Ask size." - }, - "bp": { - "existingJavaType": "java.lang.Double", - "javaName": "bidPrice", - "title": "Bid price." - }, - "bs": { - "existingJavaType": "java.lang.Double", - "javaName": "bidSize", - "title": "Bid size." - } - } -} diff --git a/schema_json/endpoint/market_data/crypto/historical/trade/crypto_trade.json b/schema_json/endpoint/market_data/crypto/historical/trade/crypto_trade.json new file mode 100644 index 00000000..0b0c1b53 --- /dev/null +++ b/schema_json/endpoint/market_data/crypto/historical/trade/crypto_trade.json @@ -0,0 +1,24 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.historical.trade.Trade" + }, + "title": "See Historical Crypto Data.", + "properties": { + "x": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.common.enums.Exchange", + "javaName": "exchange", + "title": "Exchange where the trade happened." + }, + "s": { + "existingJavaType": "java.lang.Double", + "javaName": "size", + "title": "Trade size." + }, + "tks": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.common.enums.TakerSide", + "javaName": "takerSide", + "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.common.enums.TakerSide} of the trade." + } + } +} diff --git a/schema_json/endpoint/market_data/crypto/historical/trade/trades_response.json b/schema_json/endpoint/market_data/crypto/historical/trade/crypto_trades_response.json similarity index 75% rename from schema_json/endpoint/market_data/crypto/historical/trade/trades_response.json rename to schema_json/endpoint/market_data/crypto/historical/trade/crypto_trades_response.json index 68a770a5..a512c775 100644 --- a/schema_json/endpoint/market_data/crypto/historical/trade/trades_response.json +++ b/schema_json/endpoint/market_data/crypto/historical/trade/crypto_trades_response.json @@ -1,10 +1,10 @@ { "type": "object", - "title": "See Crypto Historical Data.", + "title": "See Historical Crypto Data.", "properties": { "trades": { - "existingJavaType": "java.util.ArrayList", - "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.Trade}s." + "existingJavaType": "java.util.ArrayList", + "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.CryptoTrade}s." }, "symbol": { "existingJavaType": "java.lang.String", diff --git a/schema_json/endpoint/market_data/crypto/historical/trade/latest_trade_response.json b/schema_json/endpoint/market_data/crypto/historical/trade/latest_crypto_trade_response.json similarity index 64% rename from schema_json/endpoint/market_data/crypto/historical/trade/latest_trade_response.json rename to schema_json/endpoint/market_data/crypto/historical/trade/latest_crypto_trade_response.json index 089f9528..512c6a18 100644 --- a/schema_json/endpoint/market_data/crypto/historical/trade/latest_trade_response.json +++ b/schema_json/endpoint/market_data/crypto/historical/trade/latest_crypto_trade_response.json @@ -1,14 +1,14 @@ { "type": "object", - "title": "See Crypto Historical Data.", + "title": "See Historical Crypto Data.", "properties": { "symbol": { "existingJavaType": "java.lang.String", "title": "Symbol that was queried." }, "trade": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.Trade", - "title": "The latest {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.Trade}." + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.CryptoTrade", + "title": "The latest {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.CryptoTrade}." } } } diff --git a/schema_json/endpoint/market_data/crypto/historical/trade/trade.json b/schema_json/endpoint/market_data/crypto/historical/trade/trade.json deleted file mode 100644 index 56fef69b..00000000 --- a/schema_json/endpoint/market_data/crypto/historical/trade/trade.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "type": "object", - "title": "See Crypto Historical Data.", - "properties": { - "t": { - "existingJavaType": "java.time.ZonedDateTime", - "javaName": "timestamp", - "title": "Timestamp with nanosecond precision." - }, - "x": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.enums.Exchange", - "javaName": "exchange", - "title": "Exchange where the trade happened." - }, - "p": { - "existingJavaType": "java.lang.Double", - "javaName": "price", - "title": "Trade price." - }, - "s": { - "existingJavaType": "java.lang.Double", - "javaName": "size", - "title": "Trade size." - }, - "tks": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.enums.TakerSide", - "javaName": "takerSide", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.enums.TakerSide} of the trade." - }, - "i": { - "existingJavaType": "java.lang.Long", - "javaName": "tradeID", - "title": "Trade ID." - } - } -} diff --git a/schema_json/endpoint/market_data/crypto/historical/xbbo/xbbo.json b/schema_json/endpoint/market_data/crypto/historical/xbbo/xbbo.json index 9646a1ee..d279722f 100644 --- a/schema_json/endpoint/market_data/crypto/historical/xbbo/xbbo.json +++ b/schema_json/endpoint/market_data/crypto/historical/xbbo/xbbo.json @@ -1,6 +1,6 @@ { "type": "object", - "title": "See Crypto Historical Data.", + "title": "See Historical Crypto Data.", "properties": { "t": { "existingJavaType": "java.time.ZonedDateTime", @@ -8,7 +8,7 @@ "title": "Timestamp with nanosecond precision." }, "ax": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.enums.Exchange", + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.common.enums.Exchange", "javaName": "askExchange", "title": "Ask Exchange." }, @@ -23,7 +23,7 @@ "title": "Ask size." }, "bx": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.enums.Exchange", + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.common.enums.Exchange", "javaName": "bidExchange", "title": "Bid Exchange." }, diff --git a/schema_json/endpoint/market_data/crypto/historical/xbbo/xbbo_response.json b/schema_json/endpoint/market_data/crypto/historical/xbbo/xbbo_response.json index b3bbe427..2fdc8f28 100644 --- a/schema_json/endpoint/market_data/crypto/historical/xbbo/xbbo_response.json +++ b/schema_json/endpoint/market_data/crypto/historical/xbbo/xbbo_response.json @@ -1,6 +1,6 @@ { "type": "object", - "title": "See Crypto Historical Data.", + "title": "See Historical Crypto Data.", "properties": { "symbol": { "existingJavaType": "java.lang.String", diff --git a/schema_json/endpoint/market_data/crypto/realtime/bar/crypto_bar_message.json b/schema_json/endpoint/market_data/crypto/realtime/bar/crypto_bar_message.json new file mode 100644 index 00000000..3ef4ac16 --- /dev/null +++ b/schema_json/endpoint/market_data/crypto/realtime/bar/crypto_bar_message.json @@ -0,0 +1,29 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.bar.BarMessage" + }, + "title": "See Realtime Crypto Market Data.", + "properties": { + "x": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.common.enums.Exchange", + "javaName": "exchange", + "title": "Exchange." + }, + "v": { + "existingJavaType": "java.lang.Double", + "javaName": "volume", + "title": "Volume." + }, + "n": { + "existingJavaType": "java.lang.Long", + "javaName": "tradeCount", + "title": "Trade count." + }, + "vw": { + "existingJavaType": "java.lang.Double", + "javaName": "vwap", + "title": "VWAP (Volume Weighted Average Price)." + } + } +} diff --git a/schema_json/endpoint/market_data/crypto/realtime/quote/crypto_quote_message.json b/schema_json/endpoint/market_data/crypto/realtime/quote/crypto_quote_message.json new file mode 100644 index 00000000..9d9a2618 --- /dev/null +++ b/schema_json/endpoint/market_data/crypto/realtime/quote/crypto_quote_message.json @@ -0,0 +1,24 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.quote.QuoteMessage" + }, + "title": "See Realtime Crypto Market Data.", + "properties": { + "x": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.common.enums.Exchange", + "javaName": "exchange", + "title": "Exchange." + }, + "as": { + "existingJavaType": "java.lang.Double", + "javaName": "askSize", + "title": "Ask size." + }, + "bs": { + "existingJavaType": "java.lang.Double", + "javaName": "bidSize", + "title": "Bid size." + } + } +} diff --git a/schema_json/endpoint/market_data/crypto/realtime/trade/crypto_trade_message.json b/schema_json/endpoint/market_data/crypto/realtime/trade/crypto_trade_message.json new file mode 100644 index 00000000..ec4f5227 --- /dev/null +++ b/schema_json/endpoint/market_data/crypto/realtime/trade/crypto_trade_message.json @@ -0,0 +1,24 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.trade.TradeMessage" + }, + "title": "See Realtime Crypto Market Data.", + "properties": { + "x": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.common.enums.Exchange", + "javaName": "exchange", + "title": "Exchange where the trade happened." + }, + "s": { + "existingJavaType": "java.lang.Double", + "javaName": "size", + "title": "Trade size." + }, + "tks": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.common.enums.TakerSide", + "javaName": "takerSide", + "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.common.enums.TakerSide} of the trade." + } + } +} diff --git a/schema_json/endpoint/market_data/stock/historical/bar/enums/bar_adjustment.json b/schema_json/endpoint/market_data/stock/historical/bar/enums/bar_adjustment.json index 4f28b6ed..d42b31b2 100644 --- a/schema_json/endpoint/market_data/stock/historical/bar/enums/bar_adjustment.json +++ b/schema_json/endpoint/market_data/stock/historical/bar/enums/bar_adjustment.json @@ -1,6 +1,6 @@ { "type": "string", - "title": "See Historical Data.", + "title": "See Historical Stock Data.", "enum": [ "raw", "split", diff --git a/schema_json/endpoint/market_data/stock/historical/bar/stock_bar.json b/schema_json/endpoint/market_data/stock/historical/bar/stock_bar.json new file mode 100644 index 00000000..ae851453 --- /dev/null +++ b/schema_json/endpoint/market_data/stock/historical/bar/stock_bar.json @@ -0,0 +1,14 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.historical.bar.Bar" + }, + "title": "See Historical Stock Data.", + "properties": { + "v": { + "existingJavaType": "java.lang.Long", + "javaName": "volume", + "title": "Volume." + } + } +} diff --git a/schema_json/endpoint/market_data/stock/historical/bar/bars_response.json b/schema_json/endpoint/market_data/stock/historical/bar/stock_bars_response.json similarity index 85% rename from schema_json/endpoint/market_data/stock/historical/bar/bars_response.json rename to schema_json/endpoint/market_data/stock/historical/bar/stock_bars_response.json index ebd21e82..b23db483 100644 --- a/schema_json/endpoint/market_data/stock/historical/bar/bars_response.json +++ b/schema_json/endpoint/market_data/stock/historical/bar/stock_bars_response.json @@ -1,10 +1,10 @@ { "type": "object", - "title": "See Historical Data.", + "title": "See Historical Stock Data.", "properties": { "bars": { - "existingJavaType": "java.util.ArrayList", - "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.Bar}s." + "existingJavaType": "java.util.ArrayList", + "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.StockBar}s." }, "symbol": { "existingJavaType": "java.lang.String", diff --git a/schema_json/endpoint/market_data/stock/historical/quote/latest_quote_response.json b/schema_json/endpoint/market_data/stock/historical/quote/latest_stock_quote_response.json similarity index 76% rename from schema_json/endpoint/market_data/stock/historical/quote/latest_quote_response.json rename to schema_json/endpoint/market_data/stock/historical/quote/latest_stock_quote_response.json index 1250c5f8..4a9a3992 100644 --- a/schema_json/endpoint/market_data/stock/historical/quote/latest_quote_response.json +++ b/schema_json/endpoint/market_data/stock/historical/quote/latest_stock_quote_response.json @@ -1,14 +1,14 @@ { "type": "object", - "title": "See Historical Data.", + "title": "See Historical Stock Data.", "properties": { "symbol": { "existingJavaType": "java.lang.String", "title": "Symbol that was queried." }, "quote": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.Quote", - "title": "The latest {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.Quote}." + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.StockQuote", + "title": "The latest {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.StockQuote}." } } } diff --git a/schema_json/endpoint/market_data/stock/historical/quote/quote.json b/schema_json/endpoint/market_data/stock/historical/quote/stock_quote.json similarity index 66% rename from schema_json/endpoint/market_data/stock/historical/quote/quote.json rename to schema_json/endpoint/market_data/stock/historical/quote/stock_quote.json index 059c9746..164f093f 100644 --- a/schema_json/endpoint/market_data/stock/historical/quote/quote.json +++ b/schema_json/endpoint/market_data/stock/historical/quote/stock_quote.json @@ -1,22 +1,15 @@ { "type": "object", - "title": "See Historical Data.", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.historical.quote.Quote" + }, + "title": "See Historical Stock Data.", "properties": { - "t": { - "existingJavaType": "java.time.ZonedDateTime", - "javaName": "timestamp", - "title": "Timestamp with nanosecond precision." - }, "ax": { "existingJavaType": "java.lang.String", "javaName": "askExchange", "title": "Ask exchange." }, - "ap": { - "existingJavaType": "java.lang.Double", - "javaName": "askPrice", - "title": "Ask price." - }, "as": { "existingJavaType": "java.lang.Integer", "javaName": "askSize", @@ -27,11 +20,6 @@ "javaName": "bidExchange", "title": "Bid exchange." }, - "bp": { - "existingJavaType": "java.lang.Double", - "javaName": "bidPrice", - "title": "Bid price." - }, "bs": { "existingJavaType": "java.lang.Integer", "javaName": "bidSize", diff --git a/schema_json/endpoint/market_data/stock/historical/quote/quotes_response.json b/schema_json/endpoint/market_data/stock/historical/quote/stock_quotes_response.json similarity index 84% rename from schema_json/endpoint/market_data/stock/historical/quote/quotes_response.json rename to schema_json/endpoint/market_data/stock/historical/quote/stock_quotes_response.json index 6ba96682..418dcf6b 100644 --- a/schema_json/endpoint/market_data/stock/historical/quote/quotes_response.json +++ b/schema_json/endpoint/market_data/stock/historical/quote/stock_quotes_response.json @@ -1,10 +1,10 @@ { "type": "object", - "title": "See Historical Data.", + "title": "See Historical Stock Data.", "properties": { "quotes": { - "existingJavaType": "java.util.ArrayList", - "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.Quote}s." + "existingJavaType": "java.util.ArrayList", + "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.StockQuote}s." }, "symbol": { "existingJavaType": "java.lang.String", diff --git a/schema_json/endpoint/market_data/stock/historical/snapshot/snapshot.json b/schema_json/endpoint/market_data/stock/historical/snapshot/snapshot.json index f42b18af..9617b3de 100644 --- a/schema_json/endpoint/market_data/stock/historical/snapshot/snapshot.json +++ b/schema_json/endpoint/market_data/stock/historical/snapshot/snapshot.json @@ -1,26 +1,26 @@ { "type": "object", - "title": "See Historical Data.", + "title": "See Historical Stock Data.", "properties": { "latestTrade": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.Trade", - "title": "The latest {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.Trade}." + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.StockTrade", + "title": "The latest {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.StockTrade}." }, "latestQuote": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.Quote", - "title": "The latest {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.Quote}." + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.StockQuote", + "title": "The latest {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.StockQuote}." }, "minuteBar": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.Bar", - "title": "The minute {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.Bar}." + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.StockBar", + "title": "The minute {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.StockBar}." }, "dailyBar": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.Bar", - "title": "The daily {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.Bar}." + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.StockBar", + "title": "The daily {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.StockBar}." }, "prevDailyBar": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.Bar", - "title": "The previous daily {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.Bar}." + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.StockBar", + "title": "The previous daily {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.StockBar}." } } } diff --git a/schema_json/endpoint/market_data/stock/historical/trade/latest_trade_response.json b/schema_json/endpoint/market_data/stock/historical/trade/latest_stock_trade_response.json similarity index 76% rename from schema_json/endpoint/market_data/stock/historical/trade/latest_trade_response.json rename to schema_json/endpoint/market_data/stock/historical/trade/latest_stock_trade_response.json index f5be1b9d..b4765b53 100644 --- a/schema_json/endpoint/market_data/stock/historical/trade/latest_trade_response.json +++ b/schema_json/endpoint/market_data/stock/historical/trade/latest_stock_trade_response.json @@ -1,14 +1,14 @@ { "type": "object", - "title": "See Historical Data.", + "title": "See Historical Stock Data.", "properties": { "symbol": { "existingJavaType": "java.lang.String", "title": "Symbol that was queried." }, "trade": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.Trade", - "title": "The latest {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.Trade}." + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.StockTrade", + "title": "The latest {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.StockTrade}." } } } diff --git a/schema_json/endpoint/market_data/stock/historical/trade/trade.json b/schema_json/endpoint/market_data/stock/historical/trade/stock_trade.json similarity index 63% rename from schema_json/endpoint/market_data/stock/historical/trade/trade.json rename to schema_json/endpoint/market_data/stock/historical/trade/stock_trade.json index f7ef48ca..ea3e96cf 100644 --- a/schema_json/endpoint/market_data/stock/historical/trade/trade.json +++ b/schema_json/endpoint/market_data/stock/historical/trade/stock_trade.json @@ -1,22 +1,15 @@ { "type": "object", - "title": "See Historical Data.", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.historical.trade.Trade" + }, + "title": "See Historical Stock Data.", "properties": { - "t": { - "existingJavaType": "java.time.ZonedDateTime", - "javaName": "timestamp", - "title": "Timestamp with nanosecond precision." - }, "x": { "existingJavaType": "java.lang.String", "javaName": "exchange", "title": "Exchange where the trade happened." }, - "p": { - "existingJavaType": "java.lang.Double", - "javaName": "price", - "title": "Trade price." - }, "s": { "existingJavaType": "java.lang.Integer", "javaName": "size", @@ -27,11 +20,6 @@ "javaName": "conditions", "title": "The {@link java.util.ArrayList} of trade conditions." }, - "i": { - "existingJavaType": "java.lang.Long", - "javaName": "tradeID", - "title": "Trade ID." - }, "z": { "existingJavaType": "java.lang.String", "javaName": "tape", diff --git a/schema_json/endpoint/market_data/stock/historical/trade/trades_response.json b/schema_json/endpoint/market_data/stock/historical/trade/stock_trades_response.json similarity index 84% rename from schema_json/endpoint/market_data/stock/historical/trade/trades_response.json rename to schema_json/endpoint/market_data/stock/historical/trade/stock_trades_response.json index 503b6d9f..9b08b1da 100644 --- a/schema_json/endpoint/market_data/stock/historical/trade/trades_response.json +++ b/schema_json/endpoint/market_data/stock/historical/trade/stock_trades_response.json @@ -1,10 +1,10 @@ { "type": "object", - "title": "See Historical Data.", + "title": "See Historical Stock Data.", "properties": { "trades": { - "existingJavaType": "java.util.ArrayList", - "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.Trade}s." + "existingJavaType": "java.util.ArrayList", + "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.StockTrade}s." }, "symbol": { "existingJavaType": "java.lang.String", diff --git a/schema_json/endpoint/market_data/stock/realtime/bar/stock_bar_message.json b/schema_json/endpoint/market_data/stock/realtime/bar/stock_bar_message.json new file mode 100644 index 00000000..6bfbf62a --- /dev/null +++ b/schema_json/endpoint/market_data/stock/realtime/bar/stock_bar_message.json @@ -0,0 +1,14 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.bar.BarMessage" + }, + "title": "See Realtime Stock Market Data.", + "properties": { + "v": { + "existingJavaType": "java.lang.Long", + "javaName": "volume", + "title": "Volume." + } + } +} diff --git a/schema_json/endpoint/market_data/stock/realtime/quote/quote_message.json b/schema_json/endpoint/market_data/stock/realtime/quote/stock_quote_message.json similarity index 65% rename from schema_json/endpoint/market_data/stock/realtime/quote/quote_message.json rename to schema_json/endpoint/market_data/stock/realtime/quote/stock_quote_message.json index 3c7c650c..2633ccde 100644 --- a/schema_json/endpoint/market_data/stock/realtime/quote/quote_message.json +++ b/schema_json/endpoint/market_data/stock/realtime/quote/stock_quote_message.json @@ -1,20 +1,15 @@ { "type": "object", "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.SymbolMessage" + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.quote.QuoteMessage" }, - "title": "See Real-time Data.", + "title": "See Realtime Stock Market Data.", "properties": { "ax": { "existingJavaType": "java.lang.String", "javaName": "askExchange", "title": "Ask exchange code." }, - "ap": { - "existingJavaType": "java.lang.Double", - "javaName": "askPrice", - "title": "Ask price." - }, "as": { "existingJavaType": "java.lang.Integer", "javaName": "askSize", @@ -25,21 +20,11 @@ "javaName": "bidExchange", "title": "Bid exchange code." }, - "bp": { - "existingJavaType": "java.lang.Double", - "javaName": "bidPrice", - "title": "Bid price." - }, "bs": { "existingJavaType": "java.lang.Integer", "javaName": "bidSize", "title": "Bid size." }, - "t": { - "existingJavaType": "java.time.ZonedDateTime", - "javaName": "timestamp", - "title": "Timestamp with nanosecond precision." - }, "c": { "existingJavaType": "java.util.ArrayList", "javaName": "conditions", diff --git a/schema_json/endpoint/market_data/stock/realtime/trade/trade_message.json b/schema_json/endpoint/market_data/stock/realtime/trade/stock_trade_message.json similarity index 59% rename from schema_json/endpoint/market_data/stock/realtime/trade/trade_message.json rename to schema_json/endpoint/market_data/stock/realtime/trade/stock_trade_message.json index e72f5859..c3b0bc51 100644 --- a/schema_json/endpoint/market_data/stock/realtime/trade/trade_message.json +++ b/schema_json/endpoint/market_data/stock/realtime/trade/stock_trade_message.json @@ -1,35 +1,20 @@ { "type": "object", "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.SymbolMessage" + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.trade.TradeMessage" }, - "title": "See Real-time Data.", + "title": "See Realtime Stock Market Data.", "properties": { - "i": { - "existingJavaType": "java.lang.Integer", - "javaName": "tradeID", - "title": "Trade ID." - }, "x": { "existingJavaType": "java.lang.String", "javaName": "exchange", "title": "Exchange code where the trade occurred." }, - "p": { - "existingJavaType": "java.lang.Double", - "javaName": "price", - "title": "Trade price." - }, "s": { "existingJavaType": "java.lang.Integer", "javaName": "size", "title": "Trade size." }, - "t": { - "existingJavaType": "java.time.ZonedDateTime", - "javaName": "timestamp", - "title": "Timestamp with nanosecond precision." - }, "c": { "existingJavaType": "java.util.ArrayList", "javaName": "conditions", diff --git a/schema_json/endpoint/order/cancelled_order.json b/schema_json/endpoint/orders/cancelled_order.json similarity index 92% rename from schema_json/endpoint/order/cancelled_order.json rename to schema_json/endpoint/orders/cancelled_order.json index be79774c..cd34b921 100644 --- a/schema_json/endpoint/order/cancelled_order.json +++ b/schema_json/endpoint/orders/cancelled_order.json @@ -11,9 +11,9 @@ "title": "The HTTP status code of the cancelled order." }, "body": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.order.Order", + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.orders.Order", "javaName": "order", - "title": "The cancelled {@link net.jacobpeterson.alpaca.model.endpoint.order.Order}." + "title": "The cancelled {@link net.jacobpeterson.alpaca.model.endpoint.orders.Order}." } } } diff --git a/schema_json/endpoint/order/enums/current_order_status.json b/schema_json/endpoint/orders/enums/current_order_status.json similarity index 100% rename from schema_json/endpoint/order/enums/current_order_status.json rename to schema_json/endpoint/orders/enums/current_order_status.json diff --git a/schema_json/endpoint/order/enums/order_class.json b/schema_json/endpoint/orders/enums/order_class.json similarity index 100% rename from schema_json/endpoint/order/enums/order_class.json rename to schema_json/endpoint/orders/enums/order_class.json diff --git a/schema_json/endpoint/order/enums/order_side.json b/schema_json/endpoint/orders/enums/order_side.json similarity index 100% rename from schema_json/endpoint/order/enums/order_side.json rename to schema_json/endpoint/orders/enums/order_side.json diff --git a/schema_json/endpoint/order/enums/order_status.json b/schema_json/endpoint/orders/enums/order_status.json similarity index 100% rename from schema_json/endpoint/order/enums/order_status.json rename to schema_json/endpoint/orders/enums/order_status.json diff --git a/schema_json/endpoint/order/enums/order_time_in_force.json b/schema_json/endpoint/orders/enums/order_time_in_force.json similarity index 100% rename from schema_json/endpoint/order/enums/order_time_in_force.json rename to schema_json/endpoint/orders/enums/order_time_in_force.json diff --git a/schema_json/endpoint/order/enums/order_type.json b/schema_json/endpoint/orders/enums/order_type.json similarity index 100% rename from schema_json/endpoint/order/enums/order_type.json rename to schema_json/endpoint/orders/enums/order_type.json diff --git a/schema_json/endpoint/order/order.json b/schema_json/endpoint/orders/order.json similarity index 89% rename from schema_json/endpoint/order/order.json rename to schema_json/endpoint/orders/order.json index c0af5330..6a6f85fd 100644 --- a/schema_json/endpoint/order/order.json +++ b/schema_json/endpoint/orders/order.json @@ -82,20 +82,20 @@ "title": "Filled average price." }, "order_class": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.order.enums.OrderClass", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.order.enums.OrderClass}. For details of non-simple order classes, please see Bracket Order Overview." + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.orders.enums.OrderClass", + "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.orders.enums.OrderClass}. For details of non-simple order classes, please see Bracket Order Overview." }, "type": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.order.enums.OrderType", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.order.enums.OrderType}." + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.orders.enums.OrderType", + "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.orders.enums.OrderType}." }, "side": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.order.enums.OrderSide", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.order.enums.OrderSide}." + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.orders.enums.OrderSide", + "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.orders.enums.OrderSide}." }, "time_in_force": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.order.enums.OrderTimeInForce", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.order.enums.OrderTimeInForce}." + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.orders.enums.OrderTimeInForce", + "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.orders.enums.OrderTimeInForce}." }, "limit_price": { "existingJavaType": "java.lang.String", @@ -106,16 +106,16 @@ "title": "Stop price." }, "status": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.order.enums.OrderStatus", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.order.enums.OrderStatus}." + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.orders.enums.OrderStatus", + "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.orders.enums.OrderStatus}." }, "extended_hours": { "existingJavaType": "java.lang.Boolean", "title": "If true, eligible for execution outside regular trading hours." }, "legs": { - "existingJavaType": "java.util.ArrayList", - "title": "When querying non-simple order_class orders in a nested style, an {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.order.Order} entities associated with this order. Otherwise, null." + "existingJavaType": "java.util.ArrayList", + "title": "When querying non-simple order_class orders in a nested style, an {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.orders.Order} entities associated with this order. Otherwise, null." }, "trail_percent": { "existingJavaType": "java.lang.String", diff --git a/schema_json/endpoint/position/close_position_order.json b/schema_json/endpoint/positions/close_position_order.json similarity index 91% rename from schema_json/endpoint/position/close_position_order.json rename to schema_json/endpoint/positions/close_position_order.json index 5ce79575..3ffba883 100644 --- a/schema_json/endpoint/position/close_position_order.json +++ b/schema_json/endpoint/positions/close_position_order.json @@ -11,9 +11,9 @@ "title": "The HTTP status code of the position-closing order." }, "body": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.order.Order", + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.orders.Order", "javaName": "order", - "title": "The position-closing {@link net.jacobpeterson.alpaca.model.endpoint.order.Order}." + "title": "The position-closing {@link net.jacobpeterson.alpaca.model.endpoint.orders.Order}." } } } diff --git a/schema_json/endpoint/position/position.json b/schema_json/endpoint/positions/position.json similarity index 100% rename from schema_json/endpoint/position/position.json rename to schema_json/endpoint/positions/position.json diff --git a/schema_json/endpoint/streaming/trade/trade_update.json b/schema_json/endpoint/streaming/trade/trade_update.json index 339ea01c..32f6c324 100644 --- a/schema_json/endpoint/streaming/trade/trade_update.json +++ b/schema_json/endpoint/streaming/trade/trade_update.json @@ -20,8 +20,8 @@ "title": "The position quantity." }, "order": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.order.Order", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.order.Order}." + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.orders.Order", + "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.orders.Order}." } } } diff --git a/schema_json/endpoint/watchlist/watchlist.json b/schema_json/endpoint/watchlist/watchlist.json index 0cfa52a0..7c7e8602 100644 --- a/schema_json/endpoint/watchlist/watchlist.json +++ b/schema_json/endpoint/watchlist/watchlist.json @@ -23,7 +23,7 @@ "title": "Account ID." }, "assets": { - "existingJavaType": "java.util.ArrayList", + "existingJavaType": "java.util.ArrayList", "title": "The content of this watchlist, in the order as registered by the client." } } diff --git a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java index 67813131..63980318 100644 --- a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java +++ b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java @@ -5,22 +5,23 @@ import net.jacobpeterson.alpaca.model.properties.EndpointAPIType; import net.jacobpeterson.alpaca.properties.AlpacaProperties; import net.jacobpeterson.alpaca.rest.AlpacaClient; -import net.jacobpeterson.alpaca.rest.endpoint.AccountActivitiesEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.AccountConfigurationEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.AccountEndpoint; +import net.jacobpeterson.alpaca.rest.endpoint.accountactivities.AccountActivitiesEndpoint; +import net.jacobpeterson.alpaca.rest.endpoint.accountconfiguration.AccountConfigurationEndpoint; +import net.jacobpeterson.alpaca.rest.endpoint.account.AccountEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.AssetsEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.CalendarEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.ClockEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.OrdersEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.PortfolioHistoryEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.PositionsEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.WatchlistEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.marketdata.CryptoMarketDataEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.marketdata.StockMarketDataEndpoint; +import net.jacobpeterson.alpaca.rest.endpoint.assets.AssetsEndpoint; +import net.jacobpeterson.alpaca.rest.endpoint.calendar.CalendarEndpoint; +import net.jacobpeterson.alpaca.rest.endpoint.clock.ClockEndpoint; +import net.jacobpeterson.alpaca.rest.endpoint.orders.OrdersEndpoint; +import net.jacobpeterson.alpaca.rest.endpoint.portfoliohistory.PortfolioHistoryEndpoint; +import net.jacobpeterson.alpaca.rest.endpoint.positions.PositionsEndpoint; +import net.jacobpeterson.alpaca.rest.endpoint.watchlist.WatchlistEndpoint; +import net.jacobpeterson.alpaca.rest.endpoint.marketdata.crypto.CryptoMarketDataEndpoint; +import net.jacobpeterson.alpaca.rest.endpoint.marketdata.stock.StockMarketDataEndpoint; import net.jacobpeterson.alpaca.websocket.AlpacaWebsocket; -import net.jacobpeterson.alpaca.websocket.marketdata.MarketDataWebsocket; import net.jacobpeterson.alpaca.websocket.marketdata.MarketDataWebsocketInterface; +import net.jacobpeterson.alpaca.websocket.marketdata.crypto.CryptoMarketDataWebsocket; +import net.jacobpeterson.alpaca.websocket.marketdata.stock.StockMarketDataWebsocket; import net.jacobpeterson.alpaca.websocket.streaming.StreamingWebsocket; import net.jacobpeterson.alpaca.websocket.streaming.StreamingWebsocketInterface; import okhttp3.OkHttpClient; @@ -63,7 +64,8 @@ public class AlpacaAPI { private final AccountActivitiesEndpoint accountActivitiesEndpoint; private final PortfolioHistoryEndpoint portfolioHistoryEndpoint; private final StreamingWebsocket streamingWebsocket; - private final MarketDataWebsocket marketDataWebsocket; + private final CryptoMarketDataWebsocket cryptoMarketDataWebsocket; + private final StockMarketDataWebsocket stockMarketDataWebsocket; /** * Instantiates a new {@link AlpacaAPI} using properties specified in alpaca.properties file (or their @@ -178,9 +180,12 @@ public AlpacaAPI(OkHttpClient okHttpClient, String keyID, String secretKey, Stri accountConfigurationEndpoint = new AccountConfigurationEndpoint(brokerClient); accountActivitiesEndpoint = new AccountActivitiesEndpoint(brokerClient); portfolioHistoryEndpoint = new PortfolioHistoryEndpoint(brokerClient); + streamingWebsocket = new StreamingWebsocket(okHttpClient, brokerHostSubdomain, keyID, secretKey, oAuthToken); - marketDataWebsocket = stockDataClient == null ? null : - new MarketDataWebsocket(okHttpClient, dataAPIType, keyID, secretKey); + cryptoMarketDataWebsocket = cryptoDataClient == null ? null : + new CryptoMarketDataWebsocket(okHttpClient, keyID, secretKey); + stockMarketDataWebsocket = stockDataClient == null ? null : + new StockMarketDataWebsocket(okHttpClient, dataAPIType, keyID, secretKey); } /** @@ -275,10 +280,17 @@ public StreamingWebsocketInterface streaming() { } /** - * @return the {@link MarketDataWebsocketInterface} + * @return the Crypto {@link MarketDataWebsocketInterface} + */ + public MarketDataWebsocketInterface cryptoMarketDataStreaming() { + return cryptoMarketDataWebsocket; + } + + /** + * @return the Stock {@link MarketDataWebsocketInterface} */ - public MarketDataWebsocketInterface marketDataStreaming() { - return marketDataWebsocket; + public MarketDataWebsocketInterface stockMarketDataStreaming() { + return stockMarketDataWebsocket; } public OkHttpClient getOkHttpClient() { diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AccountEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/account/AccountEndpoint.java similarity index 90% rename from src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AccountEndpoint.java rename to src/main/java/net/jacobpeterson/alpaca/rest/endpoint/account/AccountEndpoint.java index ff57ff67..8054ab4f 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AccountEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/account/AccountEndpoint.java @@ -1,8 +1,9 @@ -package net.jacobpeterson.alpaca.rest.endpoint; +package net.jacobpeterson.alpaca.rest.endpoint.account; import net.jacobpeterson.alpaca.model.endpoint.account.Account; import net.jacobpeterson.alpaca.rest.AlpacaClient; import net.jacobpeterson.alpaca.rest.AlpacaClientException; +import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; import okhttp3.HttpUrl; import okhttp3.Request; diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AccountActivitiesEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/accountactivities/AccountActivitiesEndpoint.java similarity index 97% rename from src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AccountActivitiesEndpoint.java rename to src/main/java/net/jacobpeterson/alpaca/rest/endpoint/accountactivities/AccountActivitiesEndpoint.java index adbe8414..c56375bf 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AccountActivitiesEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/accountactivities/AccountActivitiesEndpoint.java @@ -1,4 +1,4 @@ -package net.jacobpeterson.alpaca.rest.endpoint; +package net.jacobpeterson.alpaca.rest.endpoint.accountactivities; import com.google.gson.JsonArray; import com.google.gson.JsonElement; @@ -10,6 +10,7 @@ import net.jacobpeterson.alpaca.model.endpoint.common.enums.SortDirection; import net.jacobpeterson.alpaca.rest.AlpacaClient; import net.jacobpeterson.alpaca.rest.AlpacaClientException; +import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; import okhttp3.HttpUrl; import okhttp3.Request; import org.slf4j.Logger; diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AccountConfigurationEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/accountconfiguration/AccountConfigurationEndpoint.java similarity index 94% rename from src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AccountConfigurationEndpoint.java rename to src/main/java/net/jacobpeterson/alpaca/rest/endpoint/accountconfiguration/AccountConfigurationEndpoint.java index 2d7c5de0..a319e650 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AccountConfigurationEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/accountconfiguration/AccountConfigurationEndpoint.java @@ -1,8 +1,9 @@ -package net.jacobpeterson.alpaca.rest.endpoint; +package net.jacobpeterson.alpaca.rest.endpoint.accountconfiguration; import net.jacobpeterson.alpaca.model.endpoint.accountconfiguration.AccountConfiguration; import net.jacobpeterson.alpaca.rest.AlpacaClient; import net.jacobpeterson.alpaca.rest.AlpacaClientException; +import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; import net.jacobpeterson.alpaca.util.okhttp.JSONBodyBuilder; import okhttp3.HttpUrl; import okhttp3.Request; diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AssetsEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/assets/AssetsEndpoint.java similarity index 88% rename from src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AssetsEndpoint.java rename to src/main/java/net/jacobpeterson/alpaca/rest/endpoint/assets/AssetsEndpoint.java index 6cbf3298..bfcfaffe 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AssetsEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/assets/AssetsEndpoint.java @@ -1,11 +1,12 @@ -package net.jacobpeterson.alpaca.rest.endpoint; +package net.jacobpeterson.alpaca.rest.endpoint.assets; import com.google.gson.reflect.TypeToken; -import net.jacobpeterson.alpaca.model.endpoint.asset.Asset; -import net.jacobpeterson.alpaca.model.endpoint.asset.enums.AssetClass; -import net.jacobpeterson.alpaca.model.endpoint.asset.enums.AssetStatus; +import net.jacobpeterson.alpaca.model.endpoint.assets.Asset; +import net.jacobpeterson.alpaca.model.endpoint.assets.enums.AssetClass; +import net.jacobpeterson.alpaca.model.endpoint.assets.enums.AssetStatus; import net.jacobpeterson.alpaca.rest.AlpacaClient; import net.jacobpeterson.alpaca.rest.AlpacaClientException; +import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; import okhttp3.HttpUrl; import okhttp3.Request; diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/CalendarEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/calendar/CalendarEndpoint.java similarity index 94% rename from src/main/java/net/jacobpeterson/alpaca/rest/endpoint/CalendarEndpoint.java rename to src/main/java/net/jacobpeterson/alpaca/rest/endpoint/calendar/CalendarEndpoint.java index 03bb2641..58ad41af 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/CalendarEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/calendar/CalendarEndpoint.java @@ -1,9 +1,10 @@ -package net.jacobpeterson.alpaca.rest.endpoint; +package net.jacobpeterson.alpaca.rest.endpoint.calendar; import com.google.gson.reflect.TypeToken; import net.jacobpeterson.alpaca.model.endpoint.calendar.Calendar; import net.jacobpeterson.alpaca.rest.AlpacaClient; import net.jacobpeterson.alpaca.rest.AlpacaClientException; +import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; import okhttp3.HttpUrl; import okhttp3.Request; diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/ClockEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/clock/ClockEndpoint.java similarity index 91% rename from src/main/java/net/jacobpeterson/alpaca/rest/endpoint/ClockEndpoint.java rename to src/main/java/net/jacobpeterson/alpaca/rest/endpoint/clock/ClockEndpoint.java index 6e5e9ae4..368edc13 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/ClockEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/clock/ClockEndpoint.java @@ -1,8 +1,9 @@ -package net.jacobpeterson.alpaca.rest.endpoint; +package net.jacobpeterson.alpaca.rest.endpoint.clock; import net.jacobpeterson.alpaca.model.endpoint.clock.Clock; import net.jacobpeterson.alpaca.rest.AlpacaClient; import net.jacobpeterson.alpaca.rest.AlpacaClientException; +import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; import okhttp3.HttpUrl; import okhttp3.Request; diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/CryptoMarketDataEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/crypto/CryptoMarketDataEndpoint.java similarity index 83% rename from src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/CryptoMarketDataEndpoint.java rename to src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/crypto/CryptoMarketDataEndpoint.java index c044d1f8..b3136b17 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/CryptoMarketDataEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/crypto/CryptoMarketDataEndpoint.java @@ -1,15 +1,15 @@ -package net.jacobpeterson.alpaca.rest.endpoint.marketdata; - -import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.enums.BarTimePeriod; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.bar.Bar; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.bar.BarsResponse; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.enums.Exchange; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.LatestQuoteResponse; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.Quote; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.QuotesResponse; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.LatestTradeResponse; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.Trade; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.TradesResponse; +package net.jacobpeterson.alpaca.rest.endpoint.marketdata.crypto; + +import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.historical.bar.enums.BarTimePeriod; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.common.enums.Exchange; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.bar.CryptoBar; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.bar.CryptoBarsResponse; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.CryptoQuote; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.CryptoQuotesResponse; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.LatestCryptoQuoteResponse; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.CryptoTrade; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.CryptoTradesResponse; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.LatestCryptoTradeResponse; import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.xbbo.XbboResponse; import net.jacobpeterson.alpaca.rest.AlpacaClient; import net.jacobpeterson.alpaca.rest.AlpacaClientException; @@ -41,7 +41,7 @@ public CryptoMarketDataEndpoint(AlpacaClient alpacaClient) { } /** - * Gets {@link Trade} historical data for the requested crypto symbol. + * Gets {@link CryptoTrade} historical data for the requested crypto symbol. * * @param symbol the symbol to query for * @param exchanges a {@link Collection} of {@link Exchange}s to filter by. null for all exchanges. @@ -53,11 +53,11 @@ public CryptoMarketDataEndpoint(AlpacaClient alpacaClient) { * is given * @param pageToken pagination token to continue from * - * @return the {@link TradesResponse} + * @return the {@link CryptoTradesResponse} * * @throws AlpacaClientException thrown for {@link AlpacaClientException}s */ - public TradesResponse getTrades(String symbol, Collection exchanges, ZonedDateTime start, + public CryptoTradesResponse getTrades(String symbol, Collection exchanges, ZonedDateTime start, ZonedDateTime end, Integer limit, String pageToken) throws AlpacaClientException { checkNotNull(symbol); @@ -91,20 +91,20 @@ public TradesResponse getTrades(String symbol, Collection exchanges, Z Request request = alpacaClient.requestBuilder(urlBuilder.build()) .get() .build(); - return alpacaClient.requestObject(request, TradesResponse.class); + return alpacaClient.requestObject(request, CryptoTradesResponse.class); } /** - * Gets the latest {@link Trade} for the requested security. + * Gets the latest {@link CryptoTrade} for the requested security. * * @param symbol the symbol to query for - * @param exchange the {@link Exchange} to filter the latest {@link Trade} by. + * @param exchange the {@link Exchange} to filter the latest {@link CryptoTrade} by. * - * @return the {@link LatestTradeResponse} + * @return the {@link LatestCryptoTradeResponse} * * @throws AlpacaClientException thrown for {@link AlpacaClientException}s */ - public LatestTradeResponse getLatestTrade(String symbol, Exchange exchange) throws AlpacaClientException { + public LatestCryptoTradeResponse getLatestTrade(String symbol, Exchange exchange) throws AlpacaClientException { checkNotNull(symbol); checkNotNull(exchange); @@ -119,11 +119,11 @@ public LatestTradeResponse getLatestTrade(String symbol, Exchange exchange) thro Request request = alpacaClient.requestBuilder(urlBuilder.build()) .get() .build(); - return alpacaClient.requestObject(request, LatestTradeResponse.class); + return alpacaClient.requestObject(request, LatestCryptoTradeResponse.class); } /** - * Gets {@link Quote} historical data for the requested crypto symbol. + * Gets {@link CryptoQuote} historical data for the requested crypto symbol. * * @param symbol the symbol to query for * @param exchanges a {@link Collection} of {@link Exchange}s to filter by. null for all exchanges. @@ -135,11 +135,11 @@ public LatestTradeResponse getLatestTrade(String symbol, Exchange exchange) thro * is given * @param pageToken pagination token to continue from * - * @return the {@link QuotesResponse} + * @return the {@link CryptoQuotesResponse} * * @throws AlpacaClientException thrown for {@link AlpacaClientException}s */ - public QuotesResponse getQuotes(String symbol, Collection exchanges, ZonedDateTime start, + public CryptoQuotesResponse getQuotes(String symbol, Collection exchanges, ZonedDateTime start, ZonedDateTime end, Integer limit, String pageToken) throws AlpacaClientException { checkNotNull(symbol); @@ -173,20 +173,20 @@ public QuotesResponse getQuotes(String symbol, Collection exchanges, Z Request request = alpacaClient.requestBuilder(urlBuilder.build()) .get() .build(); - return alpacaClient.requestObject(request, QuotesResponse.class); + return alpacaClient.requestObject(request, CryptoQuotesResponse.class); } /** - * Gets the latest {@link Quote} for the requested security. + * Gets the latest {@link CryptoQuote} for the requested security. * * @param symbol the symbol to query for - * @param exchange the {@link Exchange} to filter the latest {@link Quote} by. + * @param exchange the {@link Exchange} to filter the latest {@link CryptoQuote} by. * - * @return the {@link LatestQuoteResponse} + * @return the {@link LatestCryptoQuoteResponse} * * @throws AlpacaClientException thrown for {@link AlpacaClientException}s */ - public LatestQuoteResponse getLatestQuote(String symbol, Exchange exchange) throws AlpacaClientException { + public LatestCryptoQuoteResponse getLatestQuote(String symbol, Exchange exchange) throws AlpacaClientException { checkNotNull(symbol); checkNotNull(exchange); @@ -201,11 +201,11 @@ public LatestQuoteResponse getLatestQuote(String symbol, Exchange exchange) thro Request request = alpacaClient.requestBuilder(urlBuilder.build()) .get() .build(); - return alpacaClient.requestObject(request, LatestQuoteResponse.class); + return alpacaClient.requestObject(request, LatestCryptoQuoteResponse.class); } /** - * Gets {@link Bar} aggregate historical data for the requested crypto. + * Gets {@link CryptoBar} aggregate historical data for the requested crypto. * * @param symbol the symbol to query for * @param exchanges a {@link Collection} of {@link Exchange}s to filter by. null for all @@ -222,11 +222,11 @@ public LatestQuoteResponse getLatestQuote(String symbol, Exchange exchange) thro * BarTimePeriod#MINUTE} for this parameter and 15 for the * barTimePeriodDuration parameter. * - * @return the {@link BarsResponse} + * @return the {@link CryptoBarsResponse} * * @throws AlpacaClientException thrown for {@link AlpacaClientException}s */ - public BarsResponse getBars(String symbol, Collection exchanges, ZonedDateTime start, Integer limit, + public CryptoBarsResponse getBars(String symbol, Collection exchanges, ZonedDateTime start, Integer limit, String pageToken, int barTimePeriodDuration, BarTimePeriod barTimePeriod) throws AlpacaClientException { checkNotNull(symbol); checkNotNull(barTimePeriod); @@ -259,7 +259,7 @@ public BarsResponse getBars(String symbol, Collection exchanges, Zoned Request request = alpacaClient.requestBuilder(urlBuilder.build()) .get() .build(); - return alpacaClient.requestObject(request, BarsResponse.class); + return alpacaClient.requestObject(request, CryptoBarsResponse.class); } /** @@ -269,7 +269,7 @@ public BarsResponse getBars(String symbol, Collection exchanges, Zoned * @param exchanges a {@link Collection} of {@link Exchange}s to filter by. If null, then only the * exchanges that can be traded on Alpaca are included in the calculation. * - * @return the {@link LatestQuoteResponse} + * @return the {@link XbboResponse} * * @throws AlpacaClientException thrown for {@link AlpacaClientException}s */ diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/StockMarketDataEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/stock/StockMarketDataEndpoint.java similarity index 84% rename from src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/StockMarketDataEndpoint.java rename to src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/stock/StockMarketDataEndpoint.java index f203a5d2..fe8a846e 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/StockMarketDataEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/stock/StockMarketDataEndpoint.java @@ -1,17 +1,17 @@ -package net.jacobpeterson.alpaca.rest.endpoint.marketdata; +package net.jacobpeterson.alpaca.rest.endpoint.marketdata.stock; import com.google.gson.reflect.TypeToken; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.enums.BarTimePeriod; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.Bar; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.BarsResponse; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.historical.bar.enums.BarTimePeriod; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.StockBar; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.StockBarsResponse; import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.enums.BarAdjustment; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.LatestQuoteResponse; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.Quote; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.QuotesResponse; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.LatestStockQuoteResponse; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.StockQuote; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.StockQuotesResponse; import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.snapshot.Snapshot; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.LatestTradeResponse; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.Trade; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.TradesResponse; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.LatestStockTradeResponse; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.StockTrade; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.StockTradesResponse; import net.jacobpeterson.alpaca.rest.AlpacaClient; import net.jacobpeterson.alpaca.rest.AlpacaClientException; import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; @@ -44,7 +44,7 @@ public StockMarketDataEndpoint(AlpacaClient alpacaClient) { } /** - * Gets {@link Trade} historical data for the requested security. + * Gets {@link StockTrade} historical data for the requested security. * * @param symbol the symbol to query for * @param start filter data equal to or after this {@link ZonedDateTime}. Fractions of a second are not @@ -55,11 +55,11 @@ public StockMarketDataEndpoint(AlpacaClient alpacaClient) { * is given * @param pageToken pagination token to continue from * - * @return the {@link TradesResponse} + * @return the {@link StockTradesResponse} * * @throws AlpacaClientException thrown for {@link AlpacaClientException}s */ - public TradesResponse getTrades(String symbol, ZonedDateTime start, ZonedDateTime end, Integer limit, + public StockTradesResponse getTrades(String symbol, ZonedDateTime start, ZonedDateTime end, Integer limit, String pageToken) throws AlpacaClientException { checkNotNull(symbol); checkNotNull(start); @@ -84,19 +84,19 @@ public TradesResponse getTrades(String symbol, ZonedDateTime start, ZonedDateTim Request request = alpacaClient.requestBuilder(urlBuilder.build()) .get() .build(); - return alpacaClient.requestObject(request, TradesResponse.class); + return alpacaClient.requestObject(request, StockTradesResponse.class); } /** - * Gets the latest {@link Trade} for the requested security. + * Gets the latest {@link StockTrade} for the requested security. * * @param symbol the symbol to query for * - * @return the {@link LatestTradeResponse} + * @return the {@link LatestStockTradeResponse} * * @throws AlpacaClientException thrown for {@link AlpacaClientException}s */ - public LatestTradeResponse getLatestTrade(String symbol) throws AlpacaClientException { + public LatestStockTradeResponse getLatestTrade(String symbol) throws AlpacaClientException { checkNotNull(symbol); HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() @@ -108,11 +108,11 @@ public LatestTradeResponse getLatestTrade(String symbol) throws AlpacaClientExce Request request = alpacaClient.requestBuilder(urlBuilder.build()) .get() .build(); - return alpacaClient.requestObject(request, LatestTradeResponse.class); + return alpacaClient.requestObject(request, LatestStockTradeResponse.class); } /** - * Gets {@link Quote} (NBBO or National Best Bid and Offer) historical data for the requested security. + * Gets {@link StockQuote} (NBBO or National Best Bid and Offer) historical data for the requested security. * * @param symbol the symbol to query for * @param start filter data equal to or after this {@link ZonedDateTime}. Fractions of a second are not @@ -123,11 +123,11 @@ public LatestTradeResponse getLatestTrade(String symbol) throws AlpacaClientExce * is given * @param pageToken pagination token to continue from * - * @return the {@link QuotesResponse} + * @return the {@link StockQuotesResponse} * * @throws AlpacaClientException thrown for {@link AlpacaClientException}s */ - public QuotesResponse getQuotes(String symbol, ZonedDateTime start, ZonedDateTime end, Integer limit, + public StockQuotesResponse getQuotes(String symbol, ZonedDateTime start, ZonedDateTime end, Integer limit, String pageToken) throws AlpacaClientException { checkNotNull(symbol); checkNotNull(start); @@ -152,19 +152,19 @@ public QuotesResponse getQuotes(String symbol, ZonedDateTime start, ZonedDateTim Request request = alpacaClient.requestBuilder(urlBuilder.build()) .get() .build(); - return alpacaClient.requestObject(request, QuotesResponse.class); + return alpacaClient.requestObject(request, StockQuotesResponse.class); } /** - * Gets the latest {@link Quote} for the requested security. + * Gets the latest {@link StockQuote} for the requested security. * * @param symbol the symbol to query for * - * @return the {@link LatestQuoteResponse} + * @return the {@link LatestStockQuoteResponse} * * @throws AlpacaClientException thrown for {@link AlpacaClientException}s */ - public LatestQuoteResponse getLatestQuote(String symbol) throws AlpacaClientException { + public LatestStockQuoteResponse getLatestQuote(String symbol) throws AlpacaClientException { checkNotNull(symbol); HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() @@ -176,11 +176,11 @@ public LatestQuoteResponse getLatestQuote(String symbol) throws AlpacaClientExce Request request = alpacaClient.requestBuilder(urlBuilder.build()) .get() .build(); - return alpacaClient.requestObject(request, LatestQuoteResponse.class); + return alpacaClient.requestObject(request, LatestStockQuoteResponse.class); } /** - * Gets {@link Bar} aggregate historical data for the requested security. + * Gets {@link StockBar} aggregate historical data for the requested security. * * @param symbol the symbol to query for * @param start filter data equal to or after this {@link ZonedDateTime}. Fractions of a second are @@ -199,11 +199,11 @@ public LatestQuoteResponse getLatestQuote(String symbol) throws AlpacaClientExce * @param barAdjustment specifies the corporate action adjustment for the stocks. Default value is {@link * BarAdjustment#RAW}. * - * @return the {@link BarsResponse} + * @return the {@link StockBarsResponse} * * @throws AlpacaClientException thrown for {@link AlpacaClientException}s */ - public BarsResponse getBars(String symbol, ZonedDateTime start, ZonedDateTime end, Integer limit, + public StockBarsResponse getBars(String symbol, ZonedDateTime start, ZonedDateTime end, Integer limit, String pageToken, int barTimePeriodDuration, BarTimePeriod barTimePeriod, BarAdjustment barAdjustment) throws AlpacaClientException { checkNotNull(symbol); @@ -236,7 +236,7 @@ public BarsResponse getBars(String symbol, ZonedDateTime start, ZonedDateTime en Request request = alpacaClient.requestBuilder(urlBuilder.build()) .get() .build(); - return alpacaClient.requestObject(request, BarsResponse.class); + return alpacaClient.requestObject(request, StockBarsResponse.class); } /** diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/OrdersEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/orders/OrdersEndpoint.java similarity index 98% rename from src/main/java/net/jacobpeterson/alpaca/rest/endpoint/OrdersEndpoint.java rename to src/main/java/net/jacobpeterson/alpaca/rest/endpoint/orders/OrdersEndpoint.java index 8715d1fc..90159bb4 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/OrdersEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/orders/OrdersEndpoint.java @@ -1,19 +1,20 @@ -package net.jacobpeterson.alpaca.rest.endpoint; +package net.jacobpeterson.alpaca.rest.endpoint.orders; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import com.google.gson.reflect.TypeToken; import net.jacobpeterson.alpaca.model.endpoint.common.enums.SortDirection; -import net.jacobpeterson.alpaca.model.endpoint.order.CancelledOrder; -import net.jacobpeterson.alpaca.model.endpoint.order.Order; -import net.jacobpeterson.alpaca.model.endpoint.order.enums.CurrentOrderStatus; -import net.jacobpeterson.alpaca.model.endpoint.order.enums.OrderClass; -import net.jacobpeterson.alpaca.model.endpoint.order.enums.OrderSide; -import net.jacobpeterson.alpaca.model.endpoint.order.enums.OrderStatus; -import net.jacobpeterson.alpaca.model.endpoint.order.enums.OrderTimeInForce; -import net.jacobpeterson.alpaca.model.endpoint.order.enums.OrderType; +import net.jacobpeterson.alpaca.model.endpoint.orders.CancelledOrder; +import net.jacobpeterson.alpaca.model.endpoint.orders.Order; +import net.jacobpeterson.alpaca.model.endpoint.orders.enums.CurrentOrderStatus; +import net.jacobpeterson.alpaca.model.endpoint.orders.enums.OrderClass; +import net.jacobpeterson.alpaca.model.endpoint.orders.enums.OrderSide; +import net.jacobpeterson.alpaca.model.endpoint.orders.enums.OrderStatus; +import net.jacobpeterson.alpaca.model.endpoint.orders.enums.OrderTimeInForce; +import net.jacobpeterson.alpaca.model.endpoint.orders.enums.OrderType; import net.jacobpeterson.alpaca.rest.AlpacaClient; import net.jacobpeterson.alpaca.rest.AlpacaClientException; +import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; import net.jacobpeterson.alpaca.util.format.FormatUtil; import net.jacobpeterson.alpaca.util.okhttp.JSONBodyBuilder; import okhttp3.HttpUrl; diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/PortfolioHistoryEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/portfoliohistory/PortfolioHistoryEndpoint.java similarity index 97% rename from src/main/java/net/jacobpeterson/alpaca/rest/endpoint/PortfolioHistoryEndpoint.java rename to src/main/java/net/jacobpeterson/alpaca/rest/endpoint/portfoliohistory/PortfolioHistoryEndpoint.java index 1d01c19d..722299f0 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/PortfolioHistoryEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/portfoliohistory/PortfolioHistoryEndpoint.java @@ -1,4 +1,4 @@ -package net.jacobpeterson.alpaca.rest.endpoint; +package net.jacobpeterson.alpaca.rest.endpoint.portfoliohistory; import net.jacobpeterson.alpaca.model.endpoint.portfoliohistory.PortfolioHistory; import net.jacobpeterson.alpaca.model.endpoint.portfoliohistory.PortfolioHistoryDataPoint; @@ -7,6 +7,7 @@ import net.jacobpeterson.alpaca.model.endpoint.portfoliohistory.enums.PortfolioTimeFrame; import net.jacobpeterson.alpaca.rest.AlpacaClient; import net.jacobpeterson.alpaca.rest.AlpacaClientException; +import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; import net.jacobpeterson.alpaca.util.format.FormatUtil; import okhttp3.HttpUrl; import okhttp3.Request; diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/PositionsEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/positions/PositionsEndpoint.java similarity index 92% rename from src/main/java/net/jacobpeterson/alpaca/rest/endpoint/PositionsEndpoint.java rename to src/main/java/net/jacobpeterson/alpaca/rest/endpoint/positions/PositionsEndpoint.java index 26d72861..cccfdfb8 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/PositionsEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/positions/PositionsEndpoint.java @@ -1,12 +1,13 @@ -package net.jacobpeterson.alpaca.rest.endpoint; +package net.jacobpeterson.alpaca.rest.endpoint.positions; import com.google.gson.reflect.TypeToken; -import net.jacobpeterson.alpaca.model.endpoint.asset.Asset; -import net.jacobpeterson.alpaca.model.endpoint.order.Order; -import net.jacobpeterson.alpaca.model.endpoint.position.ClosePositionOrder; -import net.jacobpeterson.alpaca.model.endpoint.position.Position; +import net.jacobpeterson.alpaca.model.endpoint.assets.Asset; +import net.jacobpeterson.alpaca.model.endpoint.orders.Order; +import net.jacobpeterson.alpaca.model.endpoint.positions.ClosePositionOrder; +import net.jacobpeterson.alpaca.model.endpoint.positions.Position; import net.jacobpeterson.alpaca.rest.AlpacaClient; import net.jacobpeterson.alpaca.rest.AlpacaClientException; +import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; import okhttp3.HttpUrl; import okhttp3.Request; diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/WatchlistEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/watchlist/WatchlistEndpoint.java similarity index 97% rename from src/main/java/net/jacobpeterson/alpaca/rest/endpoint/WatchlistEndpoint.java rename to src/main/java/net/jacobpeterson/alpaca/rest/endpoint/watchlist/WatchlistEndpoint.java index aeadb183..8f8762eb 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/WatchlistEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/watchlist/WatchlistEndpoint.java @@ -1,11 +1,12 @@ -package net.jacobpeterson.alpaca.rest.endpoint; +package net.jacobpeterson.alpaca.rest.endpoint.watchlist; import com.google.gson.JsonArray; import com.google.gson.reflect.TypeToken; -import net.jacobpeterson.alpaca.model.endpoint.asset.Asset; +import net.jacobpeterson.alpaca.model.endpoint.assets.Asset; import net.jacobpeterson.alpaca.model.endpoint.watchlist.Watchlist; import net.jacobpeterson.alpaca.rest.AlpacaClient; import net.jacobpeterson.alpaca.rest.AlpacaClientException; +import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; import net.jacobpeterson.alpaca.util.okhttp.JSONBodyBuilder; import okhttp3.HttpUrl; import okhttp3.Request; diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataListener.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataListener.java index 09bf4266..0f760d7e 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataListener.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataListener.java @@ -1,7 +1,7 @@ package net.jacobpeterson.alpaca.websocket.marketdata; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.MarketDataMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.enums.MarketDataMessageType; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.MarketDataMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.enums.MarketDataMessageType; import net.jacobpeterson.alpaca.websocket.AlpacaWebsocketMessageListener; /** diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java index 28cce551..836a452a 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java @@ -5,15 +5,14 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.MarketDataMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.bar.BarMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.control.ErrorMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.control.SubscriptionsMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.control.SuccessMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.enums.MarketDataMessageType; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.quote.QuoteMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.trade.TradeMessage; -import net.jacobpeterson.alpaca.model.properties.DataAPIType; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.MarketDataMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.bar.BarMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.control.ErrorMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.control.SubscriptionsMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.control.SuccessMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.enums.MarketDataMessageType; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.quote.QuoteMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.trade.TradeMessage; import net.jacobpeterson.alpaca.websocket.AlpacaWebsocket; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; @@ -22,6 +21,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.lang.reflect.Type; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; @@ -37,10 +37,11 @@ /** * {@link MarketDataWebsocket} is an {@link AlpacaWebsocket} implementation and provides the {@link * MarketDataWebsocketInterface} interface for - * Real-time - * Market Data + * Realtime Market Data for both crypto + * and stocks. */ -public class MarketDataWebsocket extends AlpacaWebsocket +public abstract class MarketDataWebsocket + extends AlpacaWebsocket implements MarketDataWebsocketInterface { private static final Logger LOGGER = LoggerFactory.getLogger(MarketDataWebsocket.class); @@ -55,18 +56,17 @@ public class MarketDataWebsocket extends AlpacaWebsocketdataAPIType. + * Creates a {@link HttpUrl} for {@link MarketDataWebsocket} with the given websocketURLPathSegments. * - * @param dataAPIType the {@link DataAPIType} + * @param websocketURLPathSegments the websocket URL path segments * * @return a {@link HttpUrl} */ - private static HttpUrl createWebsocketURL(DataAPIType dataAPIType) { + private static HttpUrl createWebsocketURL(String websocketURLPathSegments) { return new HttpUrl.Builder() .scheme("https") // HttpUrl.Builder doesn't recognize "wss" scheme, but "https" works fine .host("stream.data.alpaca.markets") - .addPathSegment("v2") - .addPathSegment(dataAPIType.toString()) + .addPathSegments(websocketURLPathSegments) .build(); } @@ -74,23 +74,37 @@ private static HttpUrl createWebsocketURL(DataAPIType dataAPIType) { private final Set subscribedTrades; private final Set subscribedQuotes; private final Set subscribedBars; + private final Type tradeClassType; + private final Type quoteClassType; + private final Type barClassType; /** * Instantiates a new {@link MarketDataWebsocket}. * - * @param okHttpClient the {@link OkHttpClient} - * @param dataAPIType the {@link DataAPIType} - * @param keyID the key ID - * @param secretKey the secret key + * @param okHttpClient the {@link OkHttpClient} + * @param websocketURLPathSegments the websocket URL path segments + * @param websocketMarketDataTypeName the websocket market data type name {@link String} + * @param keyID the key ID + * @param secretKey the secret key + * @param tradeClass the {@link TradeMessage} {@link Class} to deserialize data into + * @param quoteClass the {@link QuoteMessage} {@link Class} to deserialize data into + * @param barClass the {@link BarMessage} {@link Class} to deserialize data into */ - public MarketDataWebsocket(OkHttpClient okHttpClient, DataAPIType dataAPIType, - String keyID, String secretKey) { - super(okHttpClient, createWebsocketURL(dataAPIType), "Market Data", keyID, secretKey, null); + public MarketDataWebsocket(OkHttpClient okHttpClient, String websocketURLPathSegments, + String websocketMarketDataTypeName, String keyID, String secretKey, + Class tradeClass, Class quoteClass, + Class barClass) { + super(okHttpClient, createWebsocketURL(websocketURLPathSegments), websocketMarketDataTypeName + " Market Data", + keyID, secretKey, null); listenedMarketDataMessageTypes = new HashSet<>(); subscribedTrades = new HashSet<>(); subscribedQuotes = new HashSet<>(); subscribedBars = new HashSet<>(); + + this.tradeClassType = tradeClass; + this.quoteClassType = quoteClass; + this.barClassType = barClass; } @Override @@ -139,6 +153,8 @@ protected void sendAuthenticationMessage() { // This websocket uses string frames and not binary frames. @Override public void onMessage(@NotNull WebSocket webSocket, @NotNull String message) { + LOGGER.trace("{}", message); + JsonElement messageElement = JsonParser.parseString(message); checkState(messageElement instanceof JsonArray, "Message must be a JsonArray! Received: %s", messageElement); @@ -198,13 +214,13 @@ public void onMessage(@NotNull WebSocket webSocket, @NotNull String message) { subscribedBars); break; case TRADE: - marketDataMessage = GSON.fromJson(messageObject, TradeMessage.class); + marketDataMessage = GSON.fromJson(messageObject, tradeClassType); break; case QUOTE: - marketDataMessage = GSON.fromJson(messageObject, QuoteMessage.class); + marketDataMessage = GSON.fromJson(messageObject, quoteClassType); break; case BAR: - marketDataMessage = GSON.fromJson(messageObject, BarMessage.class); + marketDataMessage = GSON.fromJson(messageObject, barClassType); break; default: LOGGER.error("Message type {} not implemented!", marketDataMessageType); diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocketInterface.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocketInterface.java index 2eb52e63..5ffcaf3c 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocketInterface.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocketInterface.java @@ -1,10 +1,10 @@ package net.jacobpeterson.alpaca.websocket.marketdata; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.MarketDataMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.control.ErrorMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.control.SubscriptionsMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.control.SuccessMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.enums.MarketDataMessageType; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.MarketDataMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.control.ErrorMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.control.SubscriptionsMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.control.SuccessMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.enums.MarketDataMessageType; import net.jacobpeterson.alpaca.websocket.AlpacaWebsocketInterface; import java.util.Collection; diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/crypto/CryptoMarketDataWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/crypto/CryptoMarketDataWebsocket.java new file mode 100644 index 00000000..fa61d1ec --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/crypto/CryptoMarketDataWebsocket.java @@ -0,0 +1,27 @@ +package net.jacobpeterson.alpaca.websocket.marketdata.crypto; + +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.realtime.bar.CryptoBarMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.realtime.quote.CryptoQuoteMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.realtime.trade.CryptoTradeMessage; +import net.jacobpeterson.alpaca.websocket.marketdata.MarketDataWebsocket; +import okhttp3.OkHttpClient; + +/** + * {@link CryptoMarketDataWebsocket} is a {@link MarketDataWebsocket} for + * Realtime + * Crypto Market Data + */ +public class CryptoMarketDataWebsocket extends MarketDataWebsocket { + + /** + * Instantiates a new {@link CryptoMarketDataWebsocket}. + * + * @param okHttpClient the {@link OkHttpClient} + * @param keyID the key ID + * @param secretKey the secret key + */ + public CryptoMarketDataWebsocket(OkHttpClient okHttpClient, String keyID, String secretKey) { + super(okHttpClient, "v1beta1/crypto", "Crypto", keyID, secretKey, CryptoTradeMessage.class, + CryptoQuoteMessage.class, CryptoBarMessage.class); + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/stock/StockMarketDataWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/stock/StockMarketDataWebsocket.java new file mode 100644 index 00000000..4b897f0d --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/stock/StockMarketDataWebsocket.java @@ -0,0 +1,30 @@ +package net.jacobpeterson.alpaca.websocket.marketdata.stock; + +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.bar.StockBarMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.quote.StockQuoteMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.trade.StockTradeMessage; +import net.jacobpeterson.alpaca.model.properties.DataAPIType; +import net.jacobpeterson.alpaca.websocket.marketdata.MarketDataWebsocket; +import okhttp3.OkHttpClient; + +/** + * {@link StockMarketDataWebsocket} is a {@link MarketDataWebsocket} for + * Realtime + * Stock Market Data + */ +public class StockMarketDataWebsocket extends MarketDataWebsocket { + + /** + * Instantiates a new {@link StockMarketDataWebsocket}. + * + * @param okHttpClient the {@link OkHttpClient} + * @param dataAPIType the {@link DataAPIType} + * @param keyID the key ID + * @param secretKey the secret key + */ + public StockMarketDataWebsocket(OkHttpClient okHttpClient, DataAPIType dataAPIType, + String keyID, String secretKey) { + super(okHttpClient, "v2/" + dataAPIType.toString(), "Stock", keyID, secretKey, StockTradeMessage.class, + StockQuoteMessage.class, StockBarMessage.class); + } +} diff --git a/src/test/java/net/jacobpeterson/alpaca/test/live/AlpacaAPITest.java b/src/test/java/net/jacobpeterson/alpaca/test/live/AlpacaAPITest.java index dafd1016..f9760235 100644 --- a/src/test/java/net/jacobpeterson/alpaca/test/live/AlpacaAPITest.java +++ b/src/test/java/net/jacobpeterson/alpaca/test/live/AlpacaAPITest.java @@ -11,14 +11,14 @@ import net.jacobpeterson.alpaca.model.endpoint.accountconfiguration.enums.TradeConfirmEmail; import net.jacobpeterson.alpaca.model.endpoint.clock.Clock; import net.jacobpeterson.alpaca.model.endpoint.common.enums.SortDirection; -import net.jacobpeterson.alpaca.model.endpoint.order.Order; -import net.jacobpeterson.alpaca.model.endpoint.order.enums.CurrentOrderStatus; +import net.jacobpeterson.alpaca.model.endpoint.orders.Order; +import net.jacobpeterson.alpaca.model.endpoint.orders.enums.CurrentOrderStatus; import net.jacobpeterson.alpaca.rest.AlpacaClientException; -import net.jacobpeterson.alpaca.rest.endpoint.AccountActivitiesEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.AccountConfigurationEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.AccountEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.ClockEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.OrdersEndpoint; +import net.jacobpeterson.alpaca.rest.endpoint.account.AccountEndpoint; +import net.jacobpeterson.alpaca.rest.endpoint.accountactivities.AccountActivitiesEndpoint; +import net.jacobpeterson.alpaca.rest.endpoint.accountconfiguration.AccountConfigurationEndpoint; +import net.jacobpeterson.alpaca.rest.endpoint.clock.ClockEndpoint; +import net.jacobpeterson.alpaca.rest.endpoint.orders.OrdersEndpoint; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; From e39ce37b58f1d40ded1ad6a6fb220978469bfd40 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Wed, 22 Dec 2021 13:22:34 -0700 Subject: [PATCH 24/84] Update README, fix some market_data schema JSONs --- README.md | 68 ++++++++++++------- .../common/historical/bar/bar.json | 3 - .../common/realtime/bar/bar_message.json | 10 +++ .../realtime/bar/crypto_bar_message.json | 10 --- .../marketdata/MarketDataWebsocket.java | 2 - .../streaming/StreamingWebsocket.java | 6 +- 6 files changed, 54 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index ffbea854..59df6b86 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ See the [OkHttpClient Documentation](https://square.github.io/okhttp/4.x/okhttp/ ## [`AlpacaClientException`](src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClientException.java) [`AlpacaClientException`](src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClientException.java) is thrown anytime an exception occurs when using various [`AlpacaEndpoints`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AlpacaEndpoint.java). It should be caught and handled within your trading algorithm application accordingly. -## [`AccountEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AccountEndpoint.java) +## [`AccountEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/account/AccountEndpoint.java) The Account API serves important information related to an account, including account status, funds available for trade, funds available for withdrawal, and various flags relevant to an account's ability to trade. Example usage: @@ -100,14 +100,14 @@ try { } ``` -## [`CryptoMarketDataEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/CryptoMarketDataEndpoint.java) +## [`CryptoMarketDataEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/crypto/CryptoMarketDataEndpoint.java) Alpaca provides cryptocurrency data from multiple venues/exchanges, namely: Coinbase, ErisX, and FTX. Example usage: ```java try { // Get BTCUSD 50 one-hour bars starting on 12/18/2021 from Coinbase and print them out - BarsResponse btcBarsResponse = alpacaAPI.cryptoMarketData().getBars( + CryptoBarsResponse btcBarsResponse = alpacaAPI.cryptoMarketData().getBars( "BTCUSD", Arrays.asList(Exchange.COINBASE), ZonedDateTime.of(2021, 12, 18, 0, 0, 0, 0, ZoneId.of("America/New_York")), @@ -127,7 +127,7 @@ try { } ``` -## [`StockMarketDataEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/StockMarketDataEndpoint.java) +## [`StockMarketDataEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/stock/StockMarketDataEndpoint.java) The Data API v2 provides market data through an easy-to-use API for historical stock market data. Example usage: @@ -135,7 +135,7 @@ Example usage: try { // Get AAPL one hour, split-adjusted bars from 7/6/2021 market open // to 7/8/2021 market close and print them out - BarsResponse aaplBarsResponse = alpacaAPI.stockMarketData().getBars( + StockBarsResponse aaplBarsResponse = alpacaAPI.stockMarketData().getBars( "AAPL", ZonedDateTime.of(2021, 7, 6, 9, 30, 0, 0, ZoneId.of("America/New_York")), ZonedDateTime.of(2021, 7, 8, 12 + 4, 0, 0, 0, ZoneId.of("America/New_York")), @@ -147,7 +147,7 @@ try { aaplBarsResponse.getBars().forEach(System.out::println); // Get AAPL first 10 trades on 7/8/2021 at market open and print them out - TradesResponse aaplTradesResponse = alpacaAPI.stockMarketData().getTrades( + StockTradesResponse aaplTradesResponse = alpacaAPI.stockMarketData().getTrades( "AAPL", ZonedDateTime.of(2021, 7, 8, 9, 30, 0, 0, ZoneId.of("America/New_York")), ZonedDateTime.of(2021, 7, 8, 9, 31, 0, 0, ZoneId.of("America/New_York")), @@ -169,7 +169,7 @@ try { } ``` -## [`OrdersEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/OrdersEndpoint.java) +## [`OrdersEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/orders/OrdersEndpoint.java) The Orders API allows a user to monitor, place, and cancel their orders with Alpaca. Example usage: @@ -229,7 +229,7 @@ try { } ``` -## [`PositionsEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/PositionsEndpoint.java) +## [`PositionsEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/positions/PositionsEndpoint.java) The Positions API provides information about an account's current open positions. Example usage: @@ -250,7 +250,7 @@ try { } ``` -## [`AssetsEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AssetsEndpoint.java) +## [`AssetsEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/assets/AssetsEndpoint.java) The Assets API serves as the master list of assets available for trade and data consumption from Alpaca. Example usage: @@ -270,7 +270,7 @@ try { } ``` -## [`WatchlistEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/WatchlistEndpoint.java) +## [`WatchlistEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/watchlist/WatchlistEndpoint.java) The Watchlist API provides CRUD operation for the account's watchlist. Example usage: @@ -304,7 +304,7 @@ try { } ``` -## [`CalendarEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/CalendarEndpoint.java) +## [`CalendarEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/calendar/CalendarEndpoint.java) The calendar API serves the full list of market days from 1970 to 2029. Example usage: @@ -320,7 +320,7 @@ try { } ``` -## [`ClockEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/ClockEndpoint.java) +## [`ClockEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/clock/ClockEndpoint.java) The clock API serves the current market timestamp, whether the market is currently open, as well as the times of the next market open and close. Example usage: @@ -334,7 +334,7 @@ try { } ``` -## [`AccountConfigurationEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AccountConfigurationEndpoint.java) +## [`AccountConfigurationEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/accountconfiguration/AccountConfigurationEndpoint.java) The Account Configuration API provides custom configurations about your trading account settings. These configurations control various allow you to modify settings to suit your trading needs. Example usage: @@ -349,7 +349,7 @@ try { } ``` -## [`AccountActivitiesEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AccountActivitiesEndpoint.java) +## [`AccountActivitiesEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/accountactivities/AccountActivitiesEndpoint.java) The Account Activities API provides access to a historical record of transaction activities that have impacted your account. Example usage: @@ -376,7 +376,7 @@ try { } ``` -## [`PortfolioHistoryEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/PortfolioHistoryEndpoint.java) +## [`PortfolioHistoryEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/portfoliohistory/PortfolioHistoryEndpoint.java) The Portfolio History API returns the timeseries data for equity and profit loss information of the account. Example usage: @@ -399,7 +399,7 @@ try { ``` ## [`StreamingWebsocket`](src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocket.java) -Alpaca offers WebSocket streaming of order updates. +Alpaca offers websocket streaming of order updates. Example usage: ```java @@ -433,7 +433,7 @@ Thread.sleep(5000); alpacaAPI.streaming().disconnect(); ``` -## [`MarketDataWebsocket`](src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java) +## [`StockMarketDataWebsocket`](src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/stock/StockMarketDataWebsocket.java) Alpaca's Data API v2 provides websocket streaming for trades, quotes, and minute bars. This helps receive the most up-to-date market information that could help your trading strategy to act upon certain market movement. Example usage: @@ -441,27 +441,27 @@ Example usage: // Add a 'MarketDataListener' that simply prints market data information MarketDataListener marketDataListener = (messageType, message) -> System.out.printf("%s: %s\n", messageType.name(), message); -alpacaAPI.marketDataStreaming().setListener(marketDataListener); +alpacaAPI.stockMarketDataStreaming().setListener(marketDataListener); // Listen to 'SubscriptionsMessage', 'SuccessMessage', and 'ErrorMessage' control messages // that contain information about the stream's current state. Note that these are subscribed // to before the websocket is connected since these messages usually are sent -// upon websocket connection. -alpacaAPI.marketDataStreaming().subscribeToControl( +// upon websocket connection. +alpacaAPI.stockMarketDataStreaming().subscribeToControl( MarketDataMessageType.SUCCESS, MarketDataMessageType.SUBSCRIPTION, MarketDataMessageType.ERROR); // Connect the websocket and confirm authentication -alpacaAPI.marketDataStreaming().connect(); -alpacaAPI.marketDataStreaming().waitForAuthorization(5, TimeUnit.SECONDS); -if (!alpacaAPI.marketDataStreaming().isValid()) { +alpacaAPI.stockMarketDataStreaming().connect(); +alpacaAPI.stockMarketDataStreaming().waitForAuthorization(5, TimeUnit.SECONDS); +if (!alpacaAPI.stockMarketDataStreaming().isValid()) { System.out.println("Websocket not valid!"); return; } -// Listen to the AAPL and TSLA trades and all ('*') bars. -alpacaAPI.marketDataStreaming().subscribe( +// Listen to AAPL and TSLA trades and all bars via the wildcard operator ('*'). +alpacaAPI.stockMarketDataStreaming().subscribe( Arrays.asList("AAPL", "TSLA"), null, Arrays.asList("*")); @@ -470,7 +470,23 @@ alpacaAPI.marketDataStreaming().subscribe( Thread.sleep(5000); // Manually disconnect the websocket -alpacaAPI.marketDataStreaming().disconnect(); +alpacaAPI.stockMarketDataStreaming().disconnect(); +``` + +## [`CryptoMarketDataWebsocket`](src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/crypto/CryptoMarketDataWebsocket.java) +Alpaca also offers cryptocurrency websocket streaming for trades, quotes, and minute bars. + +The usage is identical to the [`StockMarketDataWebsocket`](https://github.com/Petersoj/alpaca-java/#stockmarketdatawebsocket) usage except that accessing the websocket instance via the `AlpacaAPI` instance is done using: `alpacaAPI.cryptoMarketDataStreaming()` instead of `alpacaAPI.stockMarketDataStreaming()`. + +Example usage: +```java +// The 'CryptoMarketDataWebsocket' setup is identical to the 'StockMarketDataWebsocket' setup + +// Listen to BTCUSD and ETHUSD trades and all bars via the wildcard operator ('*'). +alpacaAPI.cryptoMarketDataStreaming().subscribe( + Arrays.asList("BTCUSD", "ETHUSD"), + null, + Arrays.asList("*")); ``` # Building diff --git a/schema_json/endpoint/market_data/common/historical/bar/bar.json b/schema_json/endpoint/market_data/common/historical/bar/bar.json index b0284b57..b2f3b42f 100644 --- a/schema_json/endpoint/market_data/common/historical/bar/bar.json +++ b/schema_json/endpoint/market_data/common/historical/bar/bar.json @@ -1,8 +1,5 @@ { "type": "object", - "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.quote.QuoteMessage" - }, "title": "See Market Data.", "properties": { "t": { diff --git a/schema_json/endpoint/market_data/common/realtime/bar/bar_message.json b/schema_json/endpoint/market_data/common/realtime/bar/bar_message.json index fcf5f6c6..9df2a564 100644 --- a/schema_json/endpoint/market_data/common/realtime/bar/bar_message.json +++ b/schema_json/endpoint/market_data/common/realtime/bar/bar_message.json @@ -29,6 +29,16 @@ "existingJavaType": "java.time.ZonedDateTime", "javaName": "timestamp", "title": "The timestamp." + }, + "n": { + "existingJavaType": "java.lang.Long", + "javaName": "tradeCount", + "title": "Trade count." + }, + "vw": { + "existingJavaType": "java.lang.Double", + "javaName": "vwap", + "title": "VWAP (Volume Weighted Average Price)." } } } diff --git a/schema_json/endpoint/market_data/crypto/realtime/bar/crypto_bar_message.json b/schema_json/endpoint/market_data/crypto/realtime/bar/crypto_bar_message.json index 3ef4ac16..8886e9d4 100644 --- a/schema_json/endpoint/market_data/crypto/realtime/bar/crypto_bar_message.json +++ b/schema_json/endpoint/market_data/crypto/realtime/bar/crypto_bar_message.json @@ -14,16 +14,6 @@ "existingJavaType": "java.lang.Double", "javaName": "volume", "title": "Volume." - }, - "n": { - "existingJavaType": "java.lang.Long", - "javaName": "tradeCount", - "title": "Trade count." - }, - "vw": { - "existingJavaType": "java.lang.Double", - "javaName": "vwap", - "title": "VWAP (Volume Weighted Average Price)." } } } diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java index 836a452a..9241f066 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java @@ -172,7 +172,6 @@ public void onMessage(@NotNull WebSocket webSocket, @NotNull String message) { switch (marketDataMessageType) { case SUCCESS: marketDataMessage = GSON.fromJson(messageObject, SuccessMessage.class); - LOGGER.debug("{}", marketDataMessage); if (isSuccessMessageAuthenticated((SuccessMessage) marketDataMessage)) { LOGGER.info("{} websocket authenticated.", websocketName); @@ -202,7 +201,6 @@ public void onMessage(@NotNull WebSocket webSocket, @NotNull String message) { break; case SUBSCRIPTION: marketDataMessage = GSON.fromJson(messageObject, SubscriptionsMessage.class); - LOGGER.debug("{}", marketDataMessage); // Update 'listenedMarketDataMessageTypes' and the associated subscribed symbols lists SubscriptionsMessage subscriptionsMessage = (SubscriptionsMessage) marketDataMessage; diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocket.java index 57ede8a4..cfbf2106 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocket.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocket.java @@ -136,7 +136,8 @@ protected void sendAuthenticationMessage() { // This websocket uses binary frames and not text frames. @Override public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString byteString) { - String message = byteString.utf8(); + final String message = byteString.utf8(); + LOGGER.trace("{}", message); JsonElement messageElement = JsonParser.parseString(message); checkState(messageElement instanceof JsonObject, "Message must be a JsonObject! Received: %s", messageElement); @@ -160,7 +161,6 @@ public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString byteStri LOGGER.error("{} websocket not authenticated! Received: {}.", websocketName, streamingMessage); } else { LOGGER.info("{} websocket authenticated.", websocketName); - LOGGER.debug("{}", streamingMessage); } if (authenticationMessageFuture != null) { @@ -169,7 +169,6 @@ public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString byteStri break; case LISTENING: streamingMessage = GSON.fromJson(messageObject, ListeningMessage.class); - LOGGER.debug("{}", streamingMessage); // Remove all 'StreamingMessageType's that are no longer listened to and add new ones List currentTypes = ((ListeningMessage) streamingMessage).getData().getStreams(); @@ -180,7 +179,6 @@ public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString byteStri break; case TRADE_UPDATES: streamingMessage = GSON.fromJson(messageObject, TradeUpdateMessage.class); - LOGGER.debug("{}", streamingMessage); break; default: throw new UnsupportedOperationException(); From 24065531f43cdc8d97486226b9211f67de83970c Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Wed, 22 Dec 2021 13:23:45 -0700 Subject: [PATCH 25/84] Update to 9.0.0 --- README.md | 4 ++-- build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 59df6b86..0b17443a 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ If you are using Gradle as your build tool, add the following dependency to your ``` dependencies { - implementation group: 'net.jacobpeterson', name: 'alpaca-java', version: '8.3.3' + implementation group: 'net.jacobpeterson', name: 'alpaca-java', version: '9.0.0' } ``` @@ -27,7 +27,7 @@ If you are using Maven as your build tool, add the following dependency to your net.jacobpeterson alpaca-java - 8.3.3 + 9.0.0 compile ``` diff --git a/build.gradle b/build.gradle index 88519ffb..fcbdf568 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,7 @@ tasks.getByName("generateJsonSchema2Pojo").enabled(false) // Disable default 'js archivesBaseName = 'alpaca-java' group = 'net.jacobpeterson' -version = '8.3.3-SNAPSHOT' +version = '9.0.0-SNAPSHOT' repositories { mavenCentral() From 1da8305d39d994e8e97a3180c8fb3c9812171db8 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Mon, 27 Dec 2021 00:18:18 -0700 Subject: [PATCH 26/84] Add multi-symbol getBars method, make some inline instantiations static --- README.md | 5 +- .../stock/historical/bar/enums/bar_feed.json | 8 ++ .../bar/multi_stock_bars_response.json | 14 +++ .../alpaca/rest/AlpacaClient.java | 3 + .../rest/endpoint/assets/AssetsEndpoint.java | 5 +- .../endpoint/calendar/CalendarEndpoint.java | 5 +- .../stock/StockMarketDataEndpoint.java | 89 +++++++++++++++++-- .../rest/endpoint/orders/OrdersEndpoint.java | 13 ++- .../endpoint/positions/PositionsEndpoint.java | 11 ++- .../endpoint/watchlist/WatchlistEndpoint.java | 10 ++- 10 files changed, 144 insertions(+), 19 deletions(-) create mode 100644 schema_json/endpoint/market_data/stock/historical/bar/enums/bar_feed.json create mode 100644 schema_json/endpoint/market_data/stock/historical/bar/multi_stock_bars_response.json diff --git a/README.md b/README.md index 0b17443a..197064ec 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ Example usage: ```java try { // Get AAPL one hour, split-adjusted bars from 7/6/2021 market open - // to 7/8/2021 market close and print them out + // to 7/8/2021 market close from the SIP feed and print them out StockBarsResponse aaplBarsResponse = alpacaAPI.stockMarketData().getBars( "AAPL", ZonedDateTime.of(2021, 7, 6, 9, 30, 0, 0, ZoneId.of("America/New_York")), @@ -143,7 +143,8 @@ try { null, 1, BarTimePeriod.HOUR, - BarAdjustment.SPLIT); + BarAdjustment.SPLIT, + BarFeed.SIP); aaplBarsResponse.getBars().forEach(System.out::println); // Get AAPL first 10 trades on 7/8/2021 at market open and print them out diff --git a/schema_json/endpoint/market_data/stock/historical/bar/enums/bar_feed.json b/schema_json/endpoint/market_data/stock/historical/bar/enums/bar_feed.json new file mode 100644 index 00000000..87d8a1a4 --- /dev/null +++ b/schema_json/endpoint/market_data/stock/historical/bar/enums/bar_feed.json @@ -0,0 +1,8 @@ +{ + "type": "string", + "title": "See Historical Stock Data.", + "enum": [ + "IEX", + "SIP" + ] +} diff --git a/schema_json/endpoint/market_data/stock/historical/bar/multi_stock_bars_response.json b/schema_json/endpoint/market_data/stock/historical/bar/multi_stock_bars_response.json new file mode 100644 index 00000000..65028159 --- /dev/null +++ b/schema_json/endpoint/market_data/stock/historical/bar/multi_stock_bars_response.json @@ -0,0 +1,14 @@ +{ + "type": "object", + "title": "See Historical Stock Data.", + "properties": { + "bars": { + "existingJavaType": "java.util.HashMap>", + "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.StockBar}s of {@link java.lang.String} symbols {@link java.util.HashMap}." + }, + "next_page_token": { + "existingJavaType": "java.lang.String", + "title": "Token that can be used to query the next page." + } + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClient.java b/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClient.java index 12cb6a6f..7fe5cc27 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClient.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClient.java @@ -25,6 +25,9 @@ */ public class AlpacaClient { + public static final Predicate STATUS_CODE_200_OR_207 = (code) -> code == 200 || code == 207; + public static final Predicate STATUS_CODE_200_OR_204 = (code) -> code == 200 || code == 204; + private final OkHttpClient okHttpClient; private final HttpUrl baseURL; private final Headers requestHeaders; diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/assets/AssetsEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/assets/AssetsEndpoint.java index bfcfaffe..82763589 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/assets/AssetsEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/assets/AssetsEndpoint.java @@ -10,6 +10,7 @@ import okhttp3.HttpUrl; import okhttp3.Request; +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; @@ -20,6 +21,8 @@ */ public class AssetsEndpoint extends AlpacaEndpoint { + private static final Type ASSET_ARRAYLIST_TYPE = new TypeToken>() {}.getType(); + /** * Instantiates a new {@link AssetsEndpoint}. * @@ -54,7 +57,7 @@ public List get(AssetStatus assetStatus, AssetClass assetClass) throws Al Request request = alpacaClient.requestBuilder(urlBuilder.build()) .get() .build(); - return alpacaClient.requestObject(request, new TypeToken>() {}.getType()); + return alpacaClient.requestObject(request, ASSET_ARRAYLIST_TYPE); } /** diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/calendar/CalendarEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/calendar/CalendarEndpoint.java index 58ad41af..cc1cb5d2 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/calendar/CalendarEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/calendar/CalendarEndpoint.java @@ -8,6 +8,7 @@ import okhttp3.HttpUrl; import okhttp3.Request; +import java.lang.reflect.Type; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.ArrayList; @@ -18,6 +19,8 @@ */ public class CalendarEndpoint extends AlpacaEndpoint { + private static final Type CALENDAR_ARRAYLIST_TYPE = new TypeToken>() {}.getType(); + /** * Instantiates a new {@link CalendarEndpoint}. * @@ -63,6 +66,6 @@ public List get(LocalDate start, LocalDate end) throws AlpacaClientExc Request request = alpacaClient.requestBuilder(urlBuilder.build()) .get() .build(); - return alpacaClient.requestObject(request, new TypeToken>() {}.getType()); + return alpacaClient.requestObject(request, CALENDAR_ARRAYLIST_TYPE); } } diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/stock/StockMarketDataEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/stock/StockMarketDataEndpoint.java index fe8a846e..d47b167d 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/stock/StockMarketDataEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/stock/StockMarketDataEndpoint.java @@ -2,9 +2,11 @@ import com.google.gson.reflect.TypeToken; import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.historical.bar.enums.BarTimePeriod; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.MultiStockBarsResponse; import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.StockBar; import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.StockBarsResponse; import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.enums.BarAdjustment; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.enums.BarFeed; import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.LatestStockQuoteResponse; import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.StockQuote; import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.StockQuotesResponse; @@ -19,6 +21,7 @@ import okhttp3.HttpUrl; import okhttp3.Request; +import java.lang.reflect.Type; import java.time.ZonedDateTime; import java.util.Collection; import java.util.HashMap; @@ -34,6 +37,9 @@ */ public class StockMarketDataEndpoint extends AlpacaEndpoint { + private static final Type SNAPSHOTS_OF_STRINGS_HASHMAP_TYPE = + new TypeToken>() {}.getType(); + /** * Instantiates a new {@link StockMarketDataEndpoint}. * @@ -193,19 +199,21 @@ public LatestStockQuoteResponse getLatestQuote(String symbol) throws AlpacaClien * @param barTimePeriodDuration the duration for the given barTimePeriod parameter. e.g. for * 15Min bars, you would supply 15 for this parameter and * {@link BarTimePeriod#MINUTE} for the barTimePeriod parameter. - * @param barTimePeriod the {@link BarTimePeriod} e.g. for 15Min bars, you would supply {@link + * @param barTimePeriod the {@link BarTimePeriod}. e.g. for 15Min bars, you would supply {@link * BarTimePeriod#MINUTE} for this parameter and 15 for the * barTimePeriodDuration parameter. * @param barAdjustment specifies the corporate action adjustment for the stocks. Default value is {@link - * BarAdjustment#RAW}. + * BarAdjustment#RAW} + * @param barFeed defaults to {@link BarFeed#IEX} for Free users and {@link BarFeed#SIP} for users + * with an Unlimited subscription * * @return the {@link StockBarsResponse} * * @throws AlpacaClientException thrown for {@link AlpacaClientException}s */ public StockBarsResponse getBars(String symbol, ZonedDateTime start, ZonedDateTime end, Integer limit, - String pageToken, int barTimePeriodDuration, BarTimePeriod barTimePeriod, BarAdjustment barAdjustment) - throws AlpacaClientException { + String pageToken, int barTimePeriodDuration, BarTimePeriod barTimePeriod, BarAdjustment barAdjustment, + BarFeed barFeed) throws AlpacaClientException { checkNotNull(symbol); checkNotNull(start); checkNotNull(end); @@ -233,12 +241,83 @@ public StockBarsResponse getBars(String symbol, ZonedDateTime start, ZonedDateTi urlBuilder.addQueryParameter("adjustment", barAdjustment.toString()); } + if (barFeed != null) { + urlBuilder.addQueryParameter("feed", barFeed.toString()); + } + Request request = alpacaClient.requestBuilder(urlBuilder.build()) .get() .build(); return alpacaClient.requestObject(request, StockBarsResponse.class); } + /** + * Gets {@link StockBar} aggregate historical data for the requested securities. + * + * @param symbols a {@link Collection} of symbols to query for + * @param start filter data equal to or after this {@link ZonedDateTime}. Fractions of a second are + * not accepted. + * @param end filter data equal to or before this {@link ZonedDateTime}. Fractions of a second are + * not accepted. + * @param limit number of data points to return. Must be in range 1-10000, defaults to 1000 if + * null is given + * @param pageToken pagination token to continue from + * @param barTimePeriodDuration the duration for the given barTimePeriod parameter. e.g. for + * 15Min bars, you would supply 15 for this parameter and + * {@link BarTimePeriod#MINUTE} for the barTimePeriod parameter. + * @param barTimePeriod the {@link BarTimePeriod}. e.g. for 15Min bars, you would supply {@link + * BarTimePeriod#MINUTE} for this parameter and 15 for the + * barTimePeriodDuration parameter. + * @param barAdjustment specifies the corporate action adjustment for the stocks. Default value is {@link + * BarAdjustment#RAW} + * @param barFeed defaults to {@link BarFeed#IEX} for Free users and {@link BarFeed#SIP} for users + * with an Unlimited subscription + * + * @return the {@link MultiStockBarsResponse} + * + * @throws AlpacaClientException thrown for {@link AlpacaClientException}s + */ + public MultiStockBarsResponse getBars(Collection symbols, ZonedDateTime start, ZonedDateTime end, + Integer limit, String pageToken, int barTimePeriodDuration, BarTimePeriod barTimePeriod, + BarAdjustment barAdjustment, BarFeed barFeed) throws AlpacaClientException { + checkNotNull(symbols); + checkArgument(!symbols.isEmpty(), "'symbols' cannot be empty!"); + checkNotNull(start); + checkNotNull(end); + checkNotNull(barTimePeriod); + + HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() + .addPathSegment(endpointPathSegment) + .addPathSegment("bars"); + + urlBuilder.addQueryParameter("symbols", String.join(",", symbols)); + urlBuilder.addQueryParameter("start", FormatUtil.toRFC3339Format(start)); + urlBuilder.addQueryParameter("end", FormatUtil.toRFC3339Format(end)); + + if (limit != null) { + urlBuilder.addQueryParameter("limit", limit.toString()); + } + + if (pageToken != null) { + urlBuilder.addQueryParameter("page_token", pageToken); + } + + urlBuilder.addQueryParameter("timeframe", barTimePeriodDuration + barTimePeriod.toString()); + + if (barAdjustment != null) { + urlBuilder.addQueryParameter("adjustment", barAdjustment.toString()); + } + + if (barFeed != null) { + urlBuilder.addQueryParameter("feed", barFeed.toString()); + } + + Request request = alpacaClient.requestBuilder(urlBuilder.build()) + .get() + .build(); + return alpacaClient.requestObject(request, MultiStockBarsResponse.class); + } + /** * Gets {@link Snapshot}s of the requested securities. * @@ -261,7 +340,7 @@ public Map getSnapshots(Collection symbols) throws Alp Request request = alpacaClient.requestBuilder(urlBuilder.build()) .get() .build(); - return alpacaClient.requestObject(request, new TypeToken>() {}.getType()); + return alpacaClient.requestObject(request, SNAPSHOTS_OF_STRINGS_HASHMAP_TYPE); } /** diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/orders/OrdersEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/orders/OrdersEndpoint.java index 90159bb4..c177db71 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/orders/OrdersEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/orders/OrdersEndpoint.java @@ -20,6 +20,7 @@ import okhttp3.HttpUrl; import okhttp3.Request; +import java.lang.reflect.Type; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; @@ -28,12 +29,17 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import static net.jacobpeterson.alpaca.rest.AlpacaClient.STATUS_CODE_200_OR_204; +import static net.jacobpeterson.alpaca.rest.AlpacaClient.STATUS_CODE_200_OR_207; /** * {@link AlpacaEndpoint} for Orders. */ public class OrdersEndpoint extends AlpacaEndpoint { + private static final Type ORDER_ARRAYLIST_TYPE = new TypeToken>() {}.getType(); + private static final Type CANCELLED_ORDER_ARRAYLIST_TYPE = new TypeToken>() {}.getType(); + /** * Instantiates a new {@link OrdersEndpoint}. * @@ -97,7 +103,7 @@ public List get(CurrentOrderStatus status, Integer limit, ZonedDateTime a Request request = alpacaClient.requestBuilder(urlBuilder.build()) .get() .build(); - return alpacaClient.requestObject(request, new TypeToken>() {}.getType()); + return alpacaClient.requestObject(request, ORDER_ARRAYLIST_TYPE); } /** @@ -615,8 +621,7 @@ public List cancelAll() throws AlpacaClientException { Request request = alpacaClient.requestBuilder(urlBuilder.build()) .delete() .build(); - return alpacaClient.requestObject(request, (code) -> code == 200 || code == 207, - new TypeToken>() {}.getType()); + return alpacaClient.requestObject(request, STATUS_CODE_200_OR_207, CANCELLED_ORDER_ARRAYLIST_TYPE); } /** @@ -637,6 +642,6 @@ public void cancel(String orderID) throws AlpacaClientException { Request request = alpacaClient.requestBuilder(urlBuilder.build()) .delete() .build(); - alpacaClient.requestVoid(request, (code) -> code == 200 || code == 204); + alpacaClient.requestVoid(request, STATUS_CODE_200_OR_204); } } diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/positions/PositionsEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/positions/PositionsEndpoint.java index cccfdfb8..4ea3b096 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/positions/PositionsEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/positions/PositionsEndpoint.java @@ -11,16 +11,22 @@ import okhttp3.HttpUrl; import okhttp3.Request; +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; import static com.google.common.base.Preconditions.checkNotNull; +import static net.jacobpeterson.alpaca.rest.AlpacaClient.STATUS_CODE_200_OR_207; /** * {@link AlpacaEndpoint} for Positions. */ public class PositionsEndpoint extends AlpacaEndpoint { + private static final Type POSITION_ARRAYLIST_TYPE = new TypeToken>() {}.getType(); + private static final Type CLOSED_POSITION_ORDER_ARRAYLIST_TYPE = + new TypeToken>() {}.getType(); + /** * Instantiates a new {@link PositionsEndpoint}. * @@ -43,7 +49,7 @@ public List get() throws AlpacaClientException { Request request = alpacaClient.requestBuilder(urlBuilder.build()) .get() .build(); - return alpacaClient.requestObject(request, new TypeToken>() {}.getType()); + return alpacaClient.requestObject(request, POSITION_ARRAYLIST_TYPE); } /** @@ -89,8 +95,7 @@ public List closeAll(Boolean cancelOrders) throws AlpacaClie Request request = alpacaClient.requestBuilder(urlBuilder.build()) .delete() .build(); - return alpacaClient.requestObject(request, (code) -> code == 200 || code == 207, - new TypeToken>() {}.getType()); + return alpacaClient.requestObject(request, STATUS_CODE_200_OR_207, CLOSED_POSITION_ORDER_ARRAYLIST_TYPE); } /** diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/watchlist/WatchlistEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/watchlist/WatchlistEndpoint.java index 8f8762eb..605d651b 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/watchlist/WatchlistEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/watchlist/WatchlistEndpoint.java @@ -11,12 +11,14 @@ import okhttp3.HttpUrl; import okhttp3.Request; +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static net.jacobpeterson.alpaca.rest.AlpacaClient.STATUS_CODE_200_OR_204; /** * {@link AlpacaEndpoint} for Watchlists @@ -24,6 +26,8 @@ */ public class WatchlistEndpoint extends AlpacaEndpoint { + private static final Type WATCHLIST_ARRAYLIST_TYPE = new TypeToken>() {}.getType(); + /** * Instantiates a new {@link WatchlistEndpoint}. * @@ -46,7 +50,7 @@ public List get() throws AlpacaClientException { Request request = alpacaClient.requestBuilder(urlBuilder.build()) .get() .build(); - return alpacaClient.requestObject(request, new TypeToken>() {}.getType()); + return alpacaClient.requestObject(request, WATCHLIST_ARRAYLIST_TYPE); } /** @@ -183,7 +187,7 @@ public void delete(String watchlistID) throws AlpacaClientException { Request request = alpacaClient.requestBuilder(urlBuilder.build()) .delete() .build(); - alpacaClient.requestVoid(request, (code) -> code == 200 || code == 204); + alpacaClient.requestVoid(request, STATUS_CODE_200_OR_204); } /** @@ -207,6 +211,6 @@ public Watchlist removeSymbol(String watchlistID, String symbol) throws AlpacaCl Request request = alpacaClient.requestBuilder(urlBuilder.build()) .delete() .build(); - return alpacaClient.requestObject(request, (code) -> code == 200 || code == 204, Watchlist.class); + return alpacaClient.requestObject(request, STATUS_CODE_200_OR_204, Watchlist.class); } } From d3acf99dd783f2d4c2c34bcc354e83bad7fd5696 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Mon, 27 Dec 2021 00:18:57 -0700 Subject: [PATCH 27/84] Update to 9.1.0 --- README.md | 4 ++-- build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 197064ec..e1a3d562 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ If you are using Gradle as your build tool, add the following dependency to your ``` dependencies { - implementation group: 'net.jacobpeterson', name: 'alpaca-java', version: '9.0.0' + implementation group: 'net.jacobpeterson', name: 'alpaca-java', version: '9.1.0' } ``` @@ -27,7 +27,7 @@ If you are using Maven as your build tool, add the following dependency to your net.jacobpeterson alpaca-java - 9.0.0 + 9.1.0 compile ``` diff --git a/build.gradle b/build.gradle index fcbdf568..e9552238 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,7 @@ tasks.getByName("generateJsonSchema2Pojo").enabled(false) // Disable default 'js archivesBaseName = 'alpaca-java' group = 'net.jacobpeterson' -version = '9.0.0-SNAPSHOT' +version = '9.1.0-SNAPSHOT' repositories { mavenCentral() From 0e15c393deef5e87198b223355324464510b1714 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Thu, 10 Mar 2022 22:13:39 -0700 Subject: [PATCH 28/84] Update SLF4j version, generateBuilders in POJOs, update a Javadoc --- README.md | 12 ++++++------ build.gradle | 7 +++++-- .../stock/historical/bar/stock_bars_response.json | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e1a3d562..396ee4cc 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Javadocs Build Status CodeCov badge - GitHub + GitHub

# Overview @@ -51,7 +51,7 @@ For logging, this library uses [SLF4j](http://www.slf4j.org/) which serves as an Note that the examples below are not exhaustive. Refer to the [Javadoc](https://javadoc.io/doc/net.jacobpeterson/alpaca-java) for all classes and method signatures. ## [`AlpacaAPI`](src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java) -The [`AlpacaAPI`](src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java) class contains several instances of various [`AlpacaEndpoints`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AlpacaEndpoint.java) and [`AlpacaWebsockets`](src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java) to interface with Alpaca. You will generally only need one instance of this class in your application. Note that many methods inside the various [`AlpacaEndpoints`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AlpacaEndpoint.java) allow `null` to be passed in as a parameter if it is optional. +The [`AlpacaAPI`](src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java) class contains several instances of various [`AlpacaEndpoints`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AlpacaEndpoint.java) and [`AlpacaWebsockets`](src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java) to interface with Alpaca. You will generally only need one instance of this class in your application. Note that many methods inside the various [`AlpacaEndpoints`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AlpacaEndpoint.java) allow `null` to be passed in as a parameter if it is optional. The Alpaca API specification is located [here](https://docs.alpaca.markets/api-documentation/api-v2/) and the [`AlpacaAPI`](src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java) Javadoc is located [here](https://javadoc.io/doc/net.jacobpeterson/alpaca-java/latest/net/jacobpeterson/alpaca/AlpacaAPI.html). @@ -409,10 +409,10 @@ StreamingListener streamingListener = (messageType, message) -> System.out.printf("%s: %s\n", messageType.name(), message); alpacaAPI.streaming().setListener(streamingListener); -// Listen 'AuthorizationMessage' and 'ListeningMessage' messages that contain +// Listen 'AuthorizationMessage' and 'ListeningMessage' messages that contain // information about the stream's current state. Note that these are subscribed // to before the websocket is connected since these messages usually are sent -// upon websocket connection. +// upon websocket connection. alpacaAPI.streaming().streams(StreamingMessageType.AUTHORIZATION, StreamingMessageType.LISTENING); @@ -481,7 +481,7 @@ The usage is identical to the [`StockMarketDataWebsocket`](https://github.com/Pe Example usage: ```java -// The 'CryptoMarketDataWebsocket' setup is identical to the 'StockMarketDataWebsocket' setup +// The 'CryptoMarketDataWebsocket' setup is identical to the 'StockMarketDataWebsocket' setup // Listen to BTCUSD and ETHUSD trades and all bars via the wildcard operator ('*'). alpacaAPI.cryptoMarketDataStreaming().subscribe( @@ -523,4 +523,4 @@ Note that the live tests will modify your account minimally. It's meant to test # Contributing Contributions are welcome! -If you are creating a Pull Request, be sure to create a new branch in your forked repository for your feature or bug fix instead of committing directly to the `master` branch in your fork. +If you are creating a Pull Request, be sure to create a new branch in your forked repository for your feature or bug fix instead of committing directly to the `master` branch in your fork. diff --git a/build.gradle b/build.gradle index e9552238..bfd19fbd 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,6 @@ plugins { } apply plugin: 'jsonschema2pojo' -tasks.getByName("generateJsonSchema2Pojo").enabled(false) // Disable default 'jsonSchema2Pojo' task archivesBaseName = 'alpaca-java' group = 'net.jacobpeterson' @@ -38,7 +37,7 @@ repositories { dependencies { // Logging framework - implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.31' + implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.32' // This is a bridge for the Apache Jakarta Commons Logging library used in dependencies to actually use SLF4j instead implementation group: 'org.slf4j', name: 'jcl-over-slf4j', version: '1.7.31' @@ -178,6 +177,9 @@ processResources { // START POJO generation // +// Disable default 'jsonSchema2Pojo' task +tasks.getByName("generateJsonSchema2Pojo").enabled(false) + // The generated POJOs will be in a package structure analogous to the path in the 'schema_json/' directory // See: https://github.com/joelittlejohn/jsonschema2pojo/wiki/Reference @@ -240,6 +242,7 @@ jsonSchema2Pojo { includeGetters = true includeSetters = true includeCopyConstructor = true + generateBuilders = true } // diff --git a/schema_json/endpoint/market_data/stock/historical/bar/stock_bars_response.json b/schema_json/endpoint/market_data/stock/historical/bar/stock_bars_response.json index b23db483..5add43d2 100644 --- a/schema_json/endpoint/market_data/stock/historical/bar/stock_bars_response.json +++ b/schema_json/endpoint/market_data/stock/historical/bar/stock_bars_response.json @@ -4,7 +4,7 @@ "properties": { "bars": { "existingJavaType": "java.util.ArrayList", - "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.StockBar}s." + "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.StockBar}s. This may be null if no bars exist." }, "symbol": { "existingJavaType": "java.lang.String", From 04545575ea122a1b5ace2077b02a94e9d1664b98 Mon Sep 17 00:00:00 2001 From: jessethouin Date: Thu, 21 Apr 2022 16:18:38 -0400 Subject: [PATCH 29/84] Updated requestLimitOrder to take Double type quantity for fractional limit orders. --- .../alpaca/rest/endpoint/orders/OrdersEndpoint.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/orders/OrdersEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/orders/OrdersEndpoint.java index c177db71..3212bf75 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/orders/OrdersEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/orders/OrdersEndpoint.java @@ -285,9 +285,9 @@ public Order requestNotionalMarketOrder(String symbol, Double notional, OrderSid * * @throws AlpacaClientException thrown for {@link AlpacaClientException}s */ - public Order requestLimitOrder(String symbol, Integer quantity, OrderSide side, OrderTimeInForce timeInForce, + public Order requestLimitOrder(String symbol, Double quantity, OrderSide side, OrderTimeInForce timeInForce, Double limitPrice, Boolean extendedHours) throws AlpacaClientException { - return requestOrder(symbol, quantity.doubleValue(), null, side, OrderType.LIMIT, timeInForce, limitPrice, + return requestOrder(symbol, quantity, null, side, OrderType.LIMIT, timeInForce, limitPrice, null, null, null, extendedHours, null, OrderClass.SIMPLE, null, null, null); } From aac872e095e3af3deafe3cdd46043ca5ba2c3cb1 Mon Sep 17 00:00:00 2001 From: Dario Arena Date: Fri, 3 Jun 2022 13:24:38 +0200 Subject: [PATCH 30/84] #121 - use double type quantity in PositionsEndpoint.close() --- .../alpaca/rest/endpoint/positions/PositionsEndpoint.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/positions/PositionsEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/positions/PositionsEndpoint.java index 4ea3b096..fc8eda81 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/positions/PositionsEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/positions/PositionsEndpoint.java @@ -113,7 +113,7 @@ public List closeAll(Boolean cancelOrders) throws AlpacaClie * * @throws AlpacaClientException thrown for {@link AlpacaClientException}s */ - public Order close(String symbolOrAssetID, Integer quantity, Double percentage) throws AlpacaClientException { + public Order close(String symbolOrAssetID, Double quantity, Double percentage) throws AlpacaClientException { checkNotNull(symbolOrAssetID); HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() From 43206bdc629be8a33bd28b4dc967d0f0ff63fa08 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Mon, 20 Jun 2022 09:37:36 -0600 Subject: [PATCH 31/84] Update dependency versions --- build.gradle | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index bfd19fbd..80c4aa13 100644 --- a/build.gradle +++ b/build.gradle @@ -37,22 +37,22 @@ repositories { dependencies { // Logging framework - implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.32' + implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.36' // This is a bridge for the Apache Jakarta Commons Logging library used in dependencies to actually use SLF4j instead - implementation group: 'org.slf4j', name: 'jcl-over-slf4j', version: '1.7.31' + implementation group: 'org.slf4j', name: 'jcl-over-slf4j', version: '1.7.36' // Guava and GSON - implementation group: 'com.google.guava', name: 'guava', version: '30.1.1-jre' - implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.7' + implementation group: 'com.google.guava', name: 'guava', version: '31.1-jre' + implementation group: 'com.google.code.gson', name: 'gson', version: '2.9.0' // OkHttp - implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.9.1' + implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.10.0' implementation group: 'com.github.devcsrj', name: 'slf4j-okhttp3-logging-interceptor', version: '1.0.1' // Unit test dependencies - testImplementation group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.30' - testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter', version: '5.7.0' - testImplementation group: 'org.mockito', name: 'mockito-core', version: '3.7.0' + testImplementation group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.36' + testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter', version: '5.8.2' + testImplementation group: 'org.mockito', name: 'mockito-core', version: '4.6.1' } // Exclude any SLF4j-implementation transitive dependencies so users can use a logging framework of their choice From 4b9970ed0d50df1e18c0980dcf44d3f8797fdc00 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Tue, 9 Aug 2022 18:09:04 -0600 Subject: [PATCH 32/84] Update to 9.1.1 --- README.md | 4 ++-- build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 396ee4cc..e3f98186 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ If you are using Gradle as your build tool, add the following dependency to your ``` dependencies { - implementation group: 'net.jacobpeterson', name: 'alpaca-java', version: '9.1.0' + implementation group: 'net.jacobpeterson', name: 'alpaca-java', version: '9.1.1' } ``` @@ -27,7 +27,7 @@ If you are using Maven as your build tool, add the following dependency to your net.jacobpeterson alpaca-java - 9.1.0 + 9.1.1 compile ``` diff --git a/build.gradle b/build.gradle index 80c4aa13..692c91f3 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ apply plugin: 'jsonschema2pojo' archivesBaseName = 'alpaca-java' group = 'net.jacobpeterson' -version = '9.1.0-SNAPSHOT' +version = '9.1.1-SNAPSHOT' repositories { mavenCentral() From f058227f9d6da94c0c70ddfc9d348d8ee76c004c Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Tue, 16 Aug 2022 22:47:36 -0600 Subject: [PATCH 33/84] Fix FTX crypto exchange enum name --- .../endpoint/market_data/crypto/common/enums/exchange.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schema_json/endpoint/market_data/crypto/common/enums/exchange.json b/schema_json/endpoint/market_data/crypto/common/enums/exchange.json index ff9d94e5..b17a92f8 100644 --- a/schema_json/endpoint/market_data/crypto/common/enums/exchange.json +++ b/schema_json/endpoint/market_data/crypto/common/enums/exchange.json @@ -3,7 +3,7 @@ "title": "See Crypto Data.", "enum": [ "ERSX", - "FTX", + "FTXU", "CBSE" ], "javaEnums": [ From becccd909f93a1bc58ca6326feae5e7bb56fe629 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Tue, 16 Aug 2022 22:48:12 -0600 Subject: [PATCH 34/84] Update to 9.1.2 --- README.md | 4 ++-- build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e3f98186..057e7f0e 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ If you are using Gradle as your build tool, add the following dependency to your ``` dependencies { - implementation group: 'net.jacobpeterson', name: 'alpaca-java', version: '9.1.1' + implementation group: 'net.jacobpeterson', name: 'alpaca-java', version: '9.1.2' } ``` @@ -27,7 +27,7 @@ If you are using Maven as your build tool, add the following dependency to your net.jacobpeterson alpaca-java - 9.1.1 + 9.1.2 compile ``` diff --git a/build.gradle b/build.gradle index 692c91f3..bacd0079 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ apply plugin: 'jsonschema2pojo' archivesBaseName = 'alpaca-java' group = 'net.jacobpeterson' -version = '9.1.1-SNAPSHOT' +version = '9.1.2-SNAPSHOT' repositories { mavenCentral() From aad1af49b522ebe0d683a0e8cbc8f65140b06755 Mon Sep 17 00:00:00 2001 From: Kurru Date: Fri, 9 Dec 2022 12:10:46 -0800 Subject: [PATCH 35/84] Add nullable annotations on various fields/methods in AlpacaClientException --- .../alpaca/rest/AlpacaClientException.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClientException.java b/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClientException.java index 2114b1e8..8e34795c 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClientException.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClientException.java @@ -3,6 +3,9 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; + +import javax.annotation.Nullable; + import okhttp3.Response; import okhttp3.ResponseBody; @@ -11,10 +14,15 @@ public class AlpacaClientException extends Exception { private static final String CODE_KEY = "code"; private static final String MESSAGE_KEY = "message"; + @Nullable private String responseBody; + @Nullable private Integer responseStatusCode; + @Nullable private String responseStatusMessage; + @Nullable private Integer apiResponseCode; + @Nullable private String apiResponseMessage; /** @@ -120,18 +128,22 @@ private String parseAPIErrorResponse() { return messageBuilder.toString(); } + @Nullable public Integer getResponseStatusCode() { return responseStatusCode; } + @Nullable public String getResponseStatusMessage() { return responseStatusMessage; } + @Nullable public Integer getAPIResponseCode() { return apiResponseCode; } + @Nullable public String getAPIResponseMessage() { return apiResponseMessage; } From 00c3bea9e44de3eb4cb687eb989add1f5191b95a Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Tue, 13 Dec 2022 20:27:31 -0700 Subject: [PATCH 36/84] Remove redundant 'Nullable' annotation from private fields --- .../alpaca/rest/AlpacaClientException.java | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClientException.java b/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClientException.java index 8e34795c..4c902b05 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClientException.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClientException.java @@ -3,26 +3,20 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; - -import javax.annotation.Nullable; - import okhttp3.Response; import okhttp3.ResponseBody; +import javax.annotation.Nullable; + public class AlpacaClientException extends Exception { private static final String CODE_KEY = "code"; private static final String MESSAGE_KEY = "message"; - @Nullable private String responseBody; - @Nullable private Integer responseStatusCode; - @Nullable private String responseStatusMessage; - @Nullable private Integer apiResponseCode; - @Nullable private String apiResponseMessage; /** From 32e6f149f9ba212ea207edda415aab17b6263204 Mon Sep 17 00:00:00 2001 From: Doug Graham Date: Wed, 15 Mar 2023 08:28:19 -0500 Subject: [PATCH 37/84] This should fix Issue #131 --- schema_json/endpoint/positions/position.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/schema_json/endpoint/positions/position.json b/schema_json/endpoint/positions/position.json index 7ebb619f..1217ef51 100644 --- a/schema_json/endpoint/positions/position.json +++ b/schema_json/endpoint/positions/position.json @@ -28,6 +28,11 @@ "javaName": "quantity", "title": "The number of shares." }, + "qty_available": { + "existingJavaType": "java.lang.String", + "javaName": "quantity", + "title": "Total number of shares available minus open orders." + }, "side": { "existingJavaType": "java.lang.String", "title": "\"long\"." From b171b097f2117f1cfbc71ba9f9725cdca1f8ba67 Mon Sep 17 00:00:00 2001 From: Doug Graham Date: Wed, 15 Mar 2023 08:32:32 -0500 Subject: [PATCH 38/84] Needs a unique POJO name --- schema_json/endpoint/positions/position.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schema_json/endpoint/positions/position.json b/schema_json/endpoint/positions/position.json index 1217ef51..8ae71b33 100644 --- a/schema_json/endpoint/positions/position.json +++ b/schema_json/endpoint/positions/position.json @@ -30,7 +30,7 @@ }, "qty_available": { "existingJavaType": "java.lang.String", - "javaName": "quantity", + "javaName": "quantityAvailable", "title": "Total number of shares available minus open orders." }, "side": { From c23c6e7d05416f7f022d8f144872e2aeee89491e Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Mon, 20 Mar 2023 09:13:29 -0600 Subject: [PATCH 39/84] Update to 9.1.3 --- README.md | 4 ++-- build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 057e7f0e..98b9acf0 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ If you are using Gradle as your build tool, add the following dependency to your ``` dependencies { - implementation group: 'net.jacobpeterson', name: 'alpaca-java', version: '9.1.2' + implementation group: 'net.jacobpeterson', name: 'alpaca-java', version: '9.1.3' } ``` @@ -27,7 +27,7 @@ If you are using Maven as your build tool, add the following dependency to your net.jacobpeterson alpaca-java - 9.1.2 + 9.1.3 compile ``` diff --git a/build.gradle b/build.gradle index bacd0079..c7ec09bf 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ apply plugin: 'jsonschema2pojo' archivesBaseName = 'alpaca-java' group = 'net.jacobpeterson' -version = '9.1.2-SNAPSHOT' +version = '9.1.3-SNAPSHOT' repositories { mavenCentral() From 60f2deb5a5d9a7dea89bdbec091cedd9c94f677a Mon Sep 17 00:00:00 2001 From: Balaji Kandavel Date: Wed, 10 May 2023 23:09:49 -0400 Subject: [PATCH 40/84] Add week and month timeframes https://alpaca.markets/docs/api-references/market-data-api/stock-pricing-data/historical/#bars --- .../common/historical/bar/enums/bar_time_period.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/schema_json/endpoint/market_data/common/historical/bar/enums/bar_time_period.json b/schema_json/endpoint/market_data/common/historical/bar/enums/bar_time_period.json index cccf0508..0305b24e 100644 --- a/schema_json/endpoint/market_data/common/historical/bar/enums/bar_time_period.json +++ b/schema_json/endpoint/market_data/common/historical/bar/enums/bar_time_period.json @@ -4,7 +4,9 @@ "enum": [ "Min", "Hour", - "Day" + "Day", + "Week", + "Month" ], "javaEnums": [ { @@ -15,6 +17,12 @@ }, { "name": "DAY" + }, + { + "name": "WEEK" + }, + { + "name": "MONTH" } ] } From a5638b3800e495866a7fde5d8a656bde2017712b Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Fri, 26 May 2023 08:55:11 -0600 Subject: [PATCH 41/84] Update to 9.1.4 --- README.md | 4 ++-- build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 98b9acf0..a29ffc56 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ If you are using Gradle as your build tool, add the following dependency to your ``` dependencies { - implementation group: 'net.jacobpeterson', name: 'alpaca-java', version: '9.1.3' + implementation group: 'net.jacobpeterson', name: 'alpaca-java', version: '9.1.4' } ``` @@ -27,7 +27,7 @@ If you are using Maven as your build tool, add the following dependency to your net.jacobpeterson alpaca-java - 9.1.3 + 9.1.4 compile ``` diff --git a/build.gradle b/build.gradle index c7ec09bf..588393d3 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ apply plugin: 'jsonschema2pojo' archivesBaseName = 'alpaca-java' group = 'net.jacobpeterson' -version = '9.1.3-SNAPSHOT' +version = '9.1.4-SNAPSHOT' repositories { mavenCentral() From 1403348b3b74fe87c2769af2f187588631edda27 Mon Sep 17 00:00:00 2001 From: Jesse Thouin Date: Fri, 7 Jul 2023 15:26:25 -0400 Subject: [PATCH 42/84] Updated to use v1beta3 version of Alpaca APIs. --- .../crypto/common/enums/exchange.json | 20 ---- .../crypto/historical/bar/crypto_bar.json | 7 +- .../historical/bar/crypto_bars_response.json | 8 +- .../crypto/historical/quote/crypto_quote.json | 7 +- .../quote/crypto_quotes_response.json | 8 +- .../quote/latest_crypto_quote_response.json | 2 +- .../crypto/historical/trade/crypto_trade.json | 7 +- .../trade/crypto_trades_response.json | 8 +- .../trade/latest_crypto_trade_response.json | 2 +- .../crypto/historical/xbbo/xbbo.json | 41 ------- .../crypto/historical/xbbo/xbbo_response.json | 14 --- .../realtime/bar/crypto_bar_message.json | 5 - .../realtime/quote/crypto_quote_message.json | 5 - .../realtime/trade/crypto_trade_message.json | 5 - .../net/jacobpeterson/alpaca/AlpacaAPI.java | 4 +- .../crypto/CryptoMarketDataEndpoint.java | 107 +++++------------- .../crypto/CryptoMarketDataWebsocket.java | 4 +- 17 files changed, 42 insertions(+), 212 deletions(-) delete mode 100644 schema_json/endpoint/market_data/crypto/common/enums/exchange.json delete mode 100644 schema_json/endpoint/market_data/crypto/historical/xbbo/xbbo.json delete mode 100644 schema_json/endpoint/market_data/crypto/historical/xbbo/xbbo_response.json diff --git a/schema_json/endpoint/market_data/crypto/common/enums/exchange.json b/schema_json/endpoint/market_data/crypto/common/enums/exchange.json deleted file mode 100644 index b17a92f8..00000000 --- a/schema_json/endpoint/market_data/crypto/common/enums/exchange.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "type": "string", - "title": "See Crypto Data.", - "enum": [ - "ERSX", - "FTXU", - "CBSE" - ], - "javaEnums": [ - { - "name": "ERISX" - }, - { - "name": "FTX" - }, - { - "name": "COINBASE" - } - ] -} diff --git a/schema_json/endpoint/market_data/crypto/historical/bar/crypto_bar.json b/schema_json/endpoint/market_data/crypto/historical/bar/crypto_bar.json index e23c3ec6..1fbf8b5d 100644 --- a/schema_json/endpoint/market_data/crypto/historical/bar/crypto_bar.json +++ b/schema_json/endpoint/market_data/crypto/historical/bar/crypto_bar.json @@ -3,13 +3,8 @@ "extends": { "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.historical.bar.Bar" }, - "title": "See Historical Crypto Data.", + "title": "See Historical Crypto Data.", "properties": { - "x": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.common.enums.Exchange", - "javaName": "exchange", - "title": "Exchange." - }, "v": { "existingJavaType": "java.lang.Double", "javaName": "volume", diff --git a/schema_json/endpoint/market_data/crypto/historical/bar/crypto_bars_response.json b/schema_json/endpoint/market_data/crypto/historical/bar/crypto_bars_response.json index 677e6633..8461d5cc 100644 --- a/schema_json/endpoint/market_data/crypto/historical/bar/crypto_bars_response.json +++ b/schema_json/endpoint/market_data/crypto/historical/bar/crypto_bars_response.json @@ -1,15 +1,11 @@ { "type": "object", - "title": "See Historical Crypto Data.", + "title": "See Historical Crypto Data.", "properties": { "bars": { - "existingJavaType": "java.util.ArrayList", + "existingJavaType": "java.util.Map>", "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.bar.CryptoBar}s." }, - "symbol": { - "existingJavaType": "java.lang.String", - "title": "Symbol that was queried." - }, "next_page_token": { "existingJavaType": "java.lang.String", "title": "Token that can be used to query the next page." diff --git a/schema_json/endpoint/market_data/crypto/historical/quote/crypto_quote.json b/schema_json/endpoint/market_data/crypto/historical/quote/crypto_quote.json index 099df001..97e50e0b 100644 --- a/schema_json/endpoint/market_data/crypto/historical/quote/crypto_quote.json +++ b/schema_json/endpoint/market_data/crypto/historical/quote/crypto_quote.json @@ -3,13 +3,8 @@ "extends": { "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.historical.quote.Quote" }, - "title": "See Historical Crypto Data.", + "title": "See Historical Crypto Data.", "properties": { - "x": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.common.enums.Exchange", - "javaName": "exchange", - "title": "Exchange." - }, "as": { "existingJavaType": "java.lang.Double", "javaName": "askSize", diff --git a/schema_json/endpoint/market_data/crypto/historical/quote/crypto_quotes_response.json b/schema_json/endpoint/market_data/crypto/historical/quote/crypto_quotes_response.json index e8a67bfd..a5010f8c 100644 --- a/schema_json/endpoint/market_data/crypto/historical/quote/crypto_quotes_response.json +++ b/schema_json/endpoint/market_data/crypto/historical/quote/crypto_quotes_response.json @@ -1,15 +1,11 @@ { "type": "object", - "title": "See Historical Crypto Data.", + "title": "See Historical Crypto Data.", "properties": { "quotes": { - "existingJavaType": "java.util.ArrayList", + "existingJavaType": "java.util.Map>", "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.CryptoQuote}s." }, - "symbol": { - "existingJavaType": "java.lang.String", - "title": "Symbol that was queried." - }, "next_page_token": { "existingJavaType": "java.lang.String", "title": "Token that can be used to query the next page." diff --git a/schema_json/endpoint/market_data/crypto/historical/quote/latest_crypto_quote_response.json b/schema_json/endpoint/market_data/crypto/historical/quote/latest_crypto_quote_response.json index 64bf1d75..6db62c25 100644 --- a/schema_json/endpoint/market_data/crypto/historical/quote/latest_crypto_quote_response.json +++ b/schema_json/endpoint/market_data/crypto/historical/quote/latest_crypto_quote_response.json @@ -1,6 +1,6 @@ { "type": "object", - "title": "See Historical Crypto Data.", + "title": "See Historical Crypto Data.", "properties": { "symbol": { "existingJavaType": "java.lang.String", diff --git a/schema_json/endpoint/market_data/crypto/historical/trade/crypto_trade.json b/schema_json/endpoint/market_data/crypto/historical/trade/crypto_trade.json index 0b0c1b53..8e5bf0da 100644 --- a/schema_json/endpoint/market_data/crypto/historical/trade/crypto_trade.json +++ b/schema_json/endpoint/market_data/crypto/historical/trade/crypto_trade.json @@ -3,13 +3,8 @@ "extends": { "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.historical.trade.Trade" }, - "title": "See Historical Crypto Data.", + "title": "See Historical Crypto Data.", "properties": { - "x": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.common.enums.Exchange", - "javaName": "exchange", - "title": "Exchange where the trade happened." - }, "s": { "existingJavaType": "java.lang.Double", "javaName": "size", diff --git a/schema_json/endpoint/market_data/crypto/historical/trade/crypto_trades_response.json b/schema_json/endpoint/market_data/crypto/historical/trade/crypto_trades_response.json index a512c775..0a007487 100644 --- a/schema_json/endpoint/market_data/crypto/historical/trade/crypto_trades_response.json +++ b/schema_json/endpoint/market_data/crypto/historical/trade/crypto_trades_response.json @@ -1,15 +1,11 @@ { "type": "object", - "title": "See Historical Crypto Data.", + "title": "See Historical Crypto Data.", "properties": { "trades": { - "existingJavaType": "java.util.ArrayList", + "existingJavaType": "java.util.Map>", "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.CryptoTrade}s." }, - "symbol": { - "existingJavaType": "java.lang.String", - "title": "Symbol that was queried." - }, "next_page_token": { "existingJavaType": "java.lang.String", "title": "Token that can be used to query the next page." diff --git a/schema_json/endpoint/market_data/crypto/historical/trade/latest_crypto_trade_response.json b/schema_json/endpoint/market_data/crypto/historical/trade/latest_crypto_trade_response.json index 512c6a18..b49e4ef7 100644 --- a/schema_json/endpoint/market_data/crypto/historical/trade/latest_crypto_trade_response.json +++ b/schema_json/endpoint/market_data/crypto/historical/trade/latest_crypto_trade_response.json @@ -1,6 +1,6 @@ { "type": "object", - "title": "See Historical Crypto Data.", + "title": "See Historical Crypto Data.", "properties": { "symbol": { "existingJavaType": "java.lang.String", diff --git a/schema_json/endpoint/market_data/crypto/historical/xbbo/xbbo.json b/schema_json/endpoint/market_data/crypto/historical/xbbo/xbbo.json deleted file mode 100644 index d279722f..00000000 --- a/schema_json/endpoint/market_data/crypto/historical/xbbo/xbbo.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "type": "object", - "title": "See Historical Crypto Data.", - "properties": { - "t": { - "existingJavaType": "java.time.ZonedDateTime", - "javaName": "timestamp", - "title": "Timestamp with nanosecond precision." - }, - "ax": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.common.enums.Exchange", - "javaName": "askExchange", - "title": "Ask Exchange." - }, - "ap": { - "existingJavaType": "java.lang.Double", - "javaName": "askPrice", - "title": "Ask price." - }, - "as": { - "existingJavaType": "java.lang.Double", - "javaName": "askSize", - "title": "Ask size." - }, - "bx": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.common.enums.Exchange", - "javaName": "bidExchange", - "title": "Bid Exchange." - }, - "bp": { - "existingJavaType": "java.lang.Double", - "javaName": "bidPrice", - "title": "Bid price." - }, - "bs": { - "existingJavaType": "java.lang.Double", - "javaName": "bidSize", - "title": "Bid size." - } - } -} diff --git a/schema_json/endpoint/market_data/crypto/historical/xbbo/xbbo_response.json b/schema_json/endpoint/market_data/crypto/historical/xbbo/xbbo_response.json deleted file mode 100644 index 2fdc8f28..00000000 --- a/schema_json/endpoint/market_data/crypto/historical/xbbo/xbbo_response.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "object", - "title": "See Historical Crypto Data.", - "properties": { - "symbol": { - "existingJavaType": "java.lang.String", - "title": "Symbol that was queried." - }, - "xbbo": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.xbbo.Xbbo", - "title": "The latest {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.xbbo.Xbbo}." - } - } -} diff --git a/schema_json/endpoint/market_data/crypto/realtime/bar/crypto_bar_message.json b/schema_json/endpoint/market_data/crypto/realtime/bar/crypto_bar_message.json index 8886e9d4..82cfe47f 100644 --- a/schema_json/endpoint/market_data/crypto/realtime/bar/crypto_bar_message.json +++ b/schema_json/endpoint/market_data/crypto/realtime/bar/crypto_bar_message.json @@ -5,11 +5,6 @@ }, "title": "See Realtime Crypto Market Data.", "properties": { - "x": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.common.enums.Exchange", - "javaName": "exchange", - "title": "Exchange." - }, "v": { "existingJavaType": "java.lang.Double", "javaName": "volume", diff --git a/schema_json/endpoint/market_data/crypto/realtime/quote/crypto_quote_message.json b/schema_json/endpoint/market_data/crypto/realtime/quote/crypto_quote_message.json index 9d9a2618..1eac99c5 100644 --- a/schema_json/endpoint/market_data/crypto/realtime/quote/crypto_quote_message.json +++ b/schema_json/endpoint/market_data/crypto/realtime/quote/crypto_quote_message.json @@ -5,11 +5,6 @@ }, "title": "See Realtime Crypto Market Data.", "properties": { - "x": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.common.enums.Exchange", - "javaName": "exchange", - "title": "Exchange." - }, "as": { "existingJavaType": "java.lang.Double", "javaName": "askSize", diff --git a/schema_json/endpoint/market_data/crypto/realtime/trade/crypto_trade_message.json b/schema_json/endpoint/market_data/crypto/realtime/trade/crypto_trade_message.json index ec4f5227..09ee5a87 100644 --- a/schema_json/endpoint/market_data/crypto/realtime/trade/crypto_trade_message.json +++ b/schema_json/endpoint/market_data/crypto/realtime/trade/crypto_trade_message.json @@ -5,11 +5,6 @@ }, "title": "See Realtime Crypto Market Data.", "properties": { - "x": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.common.enums.Exchange", - "javaName": "exchange", - "title": "Exchange where the trade happened." - }, "s": { "existingJavaType": "java.lang.Double", "javaName": "size", diff --git a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java index 63980318..c71f496b 100644 --- a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java +++ b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java @@ -44,7 +44,7 @@ public class AlpacaAPI { private static final Logger LOGGER = LoggerFactory.getLogger(AlpacaAPI.class); private static final String VERSION_2_PATH_SEGMENT = "v2"; - private static final String VERSION_1_BETA_1_PATH_SEGMENT = "v1beta1"; + private static final String VERSION_1_BETA_3_PATH_SEGMENT = "v1beta3"; private final OkHttpClient okHttpClient; private final AlpacaClient brokerClient; @@ -160,7 +160,7 @@ public AlpacaAPI(OkHttpClient okHttpClient, String keyID, String secretKey, Stri if (oAuthToken == null) { brokerClient = new AlpacaClient(okHttpClient, keyID, secretKey, brokerHostSubdomain, VERSION_2_PATH_SEGMENT); - cryptoDataClient = new AlpacaClient(okHttpClient, keyID, secretKey, "data", VERSION_1_BETA_1_PATH_SEGMENT); + cryptoDataClient = new AlpacaClient(okHttpClient, keyID, secretKey, "data", VERSION_1_BETA_3_PATH_SEGMENT); stockDataClient = new AlpacaClient(okHttpClient, keyID, secretKey, "data", VERSION_2_PATH_SEGMENT); } else { brokerClient = new AlpacaClient(okHttpClient, oAuthToken, brokerHostSubdomain, VERSION_2_PATH_SEGMENT); diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/crypto/CryptoMarketDataEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/crypto/CryptoMarketDataEndpoint.java index b3136b17..0062d3fe 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/crypto/CryptoMarketDataEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/crypto/CryptoMarketDataEndpoint.java @@ -1,7 +1,6 @@ package net.jacobpeterson.alpaca.rest.endpoint.marketdata.crypto; import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.historical.bar.enums.BarTimePeriod; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.common.enums.Exchange; import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.bar.CryptoBar; import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.bar.CryptoBarsResponse; import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.CryptoQuote; @@ -10,7 +9,6 @@ import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.CryptoTrade; import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.CryptoTradesResponse; import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.LatestCryptoTradeResponse; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.xbbo.XbboResponse; import net.jacobpeterson.alpaca.rest.AlpacaClient; import net.jacobpeterson.alpaca.rest.AlpacaClientException; import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; @@ -26,10 +24,11 @@ /** * {@link AlpacaEndpoint} for - * Historical + * Historical * Crypto Market Data API. */ public class CryptoMarketDataEndpoint extends AlpacaEndpoint { + public static final String LOCALE = "us"; /** * Instantiates a new {@link CryptoMarketDataEndpoint}. @@ -43,8 +42,7 @@ public CryptoMarketDataEndpoint(AlpacaClient alpacaClient) { /** * Gets {@link CryptoTrade} historical data for the requested crypto symbol. * - * @param symbol the symbol to query for - * @param exchanges a {@link Collection} of {@link Exchange}s to filter by. null for all exchanges. + * @param symbols a {@link Collection} of symbols to query for * @param start filter data equal to or after this {@link ZonedDateTime}. Fractions of a second are not * accepted. null for the current day in Central Time. * @param end filter data equal to or before this {@link ZonedDateTime}. Fractions of a second are not @@ -57,20 +55,16 @@ public CryptoMarketDataEndpoint(AlpacaClient alpacaClient) { * * @throws AlpacaClientException thrown for {@link AlpacaClientException}s */ - public CryptoTradesResponse getTrades(String symbol, Collection exchanges, ZonedDateTime start, + public CryptoTradesResponse getTrades(Collection symbols, ZonedDateTime start, ZonedDateTime end, Integer limit, String pageToken) throws AlpacaClientException { - checkNotNull(symbol); + checkNotNull(symbols); HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() .addPathSegment(endpointPathSegment) - .addPathSegment(symbol) + .addPathSegment(LOCALE) .addPathSegment("trades"); - if (exchanges != null) { - urlBuilder.addQueryParameter("exchanges", exchanges.stream() - .map(Exchange::value) - .collect(Collectors.joining(","))); - } + urlBuilder.addQueryParameter("symbols", String.join(",", symbols)); if (start != null) { urlBuilder.addQueryParameter("start", FormatUtil.toRFC3339Format(start)); @@ -97,24 +91,22 @@ public CryptoTradesResponse getTrades(String symbol, Collection exchan /** * Gets the latest {@link CryptoTrade} for the requested security. * - * @param symbol the symbol to query for - * @param exchange the {@link Exchange} to filter the latest {@link CryptoTrade} by. + * @param symbols a {@link Collection} of symbols to query for * * @return the {@link LatestCryptoTradeResponse} * * @throws AlpacaClientException thrown for {@link AlpacaClientException}s */ - public LatestCryptoTradeResponse getLatestTrade(String symbol, Exchange exchange) throws AlpacaClientException { - checkNotNull(symbol); - checkNotNull(exchange); + public LatestCryptoTradeResponse getLatestTrade(Collection symbols) throws AlpacaClientException { + checkNotNull(symbols); HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() .addPathSegment(endpointPathSegment) - .addPathSegment(symbol) + .addPathSegment(LOCALE) .addPathSegment("trades") .addPathSegment("latest"); - urlBuilder.addQueryParameter("exchange", exchange.toString()); + urlBuilder.addQueryParameter("symbols", String.join(",", symbols)); Request request = alpacaClient.requestBuilder(urlBuilder.build()) .get() @@ -125,8 +117,7 @@ public LatestCryptoTradeResponse getLatestTrade(String symbol, Exchange exchange /** * Gets {@link CryptoQuote} historical data for the requested crypto symbol. * - * @param symbol the symbol to query for - * @param exchanges a {@link Collection} of {@link Exchange}s to filter by. null for all exchanges. + * @param symbols a {@link Collection} of symbols to query for * @param start filter data equal to or after this {@link ZonedDateTime}. Fractions of a second are not * accepted. null for the current day in Central Time. * @param end filter data equal to or before this {@link ZonedDateTime}. Fractions of a second are not @@ -139,20 +130,16 @@ public LatestCryptoTradeResponse getLatestTrade(String symbol, Exchange exchange * * @throws AlpacaClientException thrown for {@link AlpacaClientException}s */ - public CryptoQuotesResponse getQuotes(String symbol, Collection exchanges, ZonedDateTime start, + public CryptoQuotesResponse getQuotes(Collection symbols, ZonedDateTime start, ZonedDateTime end, Integer limit, String pageToken) throws AlpacaClientException { - checkNotNull(symbol); + checkNotNull(symbols); HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() .addPathSegment(endpointPathSegment) - .addPathSegment(symbol) + .addPathSegment(LOCALE) .addPathSegment("quotes"); - if (exchanges != null) { - urlBuilder.addQueryParameter("exchanges", exchanges.stream() - .map(Exchange::value) - .collect(Collectors.joining(","))); - } + urlBuilder.addQueryParameter("symbols", String.join(",", symbols)); if (start != null) { urlBuilder.addQueryParameter("start", FormatUtil.toRFC3339Format(start)); @@ -179,24 +166,22 @@ public CryptoQuotesResponse getQuotes(String symbol, Collection exchan /** * Gets the latest {@link CryptoQuote} for the requested security. * - * @param symbol the symbol to query for - * @param exchange the {@link Exchange} to filter the latest {@link CryptoQuote} by. + * @param symbols a {@link Collection} of symbols to query for * * @return the {@link LatestCryptoQuoteResponse} * * @throws AlpacaClientException thrown for {@link AlpacaClientException}s */ - public LatestCryptoQuoteResponse getLatestQuote(String symbol, Exchange exchange) throws AlpacaClientException { - checkNotNull(symbol); - checkNotNull(exchange); + public LatestCryptoQuoteResponse getLatestQuote(Collection symbols) throws AlpacaClientException { + checkNotNull(symbols); HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() .addPathSegment(endpointPathSegment) - .addPathSegment(symbol) + .addPathSegment(LOCALE) .addPathSegment("quotes") .addPathSegment("latest"); - urlBuilder.addQueryParameter("exchange", exchange.toString()); + urlBuilder.addQueryParameter("symbols", String.join(",", symbols)); Request request = alpacaClient.requestBuilder(urlBuilder.build()) .get() @@ -207,9 +192,7 @@ public LatestCryptoQuoteResponse getLatestQuote(String symbol, Exchange exchange /** * Gets {@link CryptoBar} aggregate historical data for the requested crypto. * - * @param symbol the symbol to query for - * @param exchanges a {@link Collection} of {@link Exchange}s to filter by. null for all - * exchanges. + * @param symbols a {@link Collection} of symbols to query for * @param start filter data equal to or after this {@link ZonedDateTime}. Fractions of a second are * not accepted. null for now. * @param limit number of data points to return. Must be in range 1-10000, defaults to 1000 if @@ -226,21 +209,17 @@ public LatestCryptoQuoteResponse getLatestQuote(String symbol, Exchange exchange * * @throws AlpacaClientException thrown for {@link AlpacaClientException}s */ - public CryptoBarsResponse getBars(String symbol, Collection exchanges, ZonedDateTime start, Integer limit, + public CryptoBarsResponse getBars(Collection symbols, ZonedDateTime start, Integer limit, String pageToken, int barTimePeriodDuration, BarTimePeriod barTimePeriod) throws AlpacaClientException { - checkNotNull(symbol); + checkNotNull(symbols); checkNotNull(barTimePeriod); HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() .addPathSegment(endpointPathSegment) - .addPathSegment(symbol) + .addPathSegment(LOCALE) .addPathSegment("bars"); - if (exchanges != null) { - urlBuilder.addQueryParameter("exchanges", exchanges.stream() - .map(Exchange::value) - .collect(Collectors.joining(","))); - } + urlBuilder.addQueryParameter("symbols", String.join(",", symbols)); if (start != null) { urlBuilder.addQueryParameter("start", FormatUtil.toRFC3339Format(start)); @@ -261,36 +240,4 @@ public CryptoBarsResponse getBars(String symbol, Collection exchanges, .build(); return alpacaClient.requestObject(request, CryptoBarsResponse.class); } - - /** - * Returns the XBBO for a crypto symbol that calculates the Best Bid and Offer across multiple exchanges. - * - * @param symbol the symbol to query for - * @param exchanges a {@link Collection} of {@link Exchange}s to filter by. If null, then only the - * exchanges that can be traded on Alpaca are included in the calculation. - * - * @return the {@link XbboResponse} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public XbboResponse getXBBO(String symbol, Collection exchanges) throws AlpacaClientException { - checkNotNull(symbol); - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment) - .addPathSegment(symbol) - .addPathSegment("xbbo") - .addPathSegment("latest"); - - if (exchanges != null) { - urlBuilder.addQueryParameter("exchanges", exchanges.stream() - .map(Exchange::value) - .collect(Collectors.joining(","))); - } - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, XbboResponse.class); - } } diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/crypto/CryptoMarketDataWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/crypto/CryptoMarketDataWebsocket.java index fa61d1ec..27168332 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/crypto/CryptoMarketDataWebsocket.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/crypto/CryptoMarketDataWebsocket.java @@ -8,7 +8,7 @@ /** * {@link CryptoMarketDataWebsocket} is a {@link MarketDataWebsocket} for - * Realtime + * Realtime * Crypto Market Data */ public class CryptoMarketDataWebsocket extends MarketDataWebsocket { @@ -21,7 +21,7 @@ public class CryptoMarketDataWebsocket extends MarketDataWebsocket { * @param secretKey the secret key */ public CryptoMarketDataWebsocket(OkHttpClient okHttpClient, String keyID, String secretKey) { - super(okHttpClient, "v1beta1/crypto", "Crypto", keyID, secretKey, CryptoTradeMessage.class, + super(okHttpClient, "v1beta3/crypto/us", "Crypto", keyID, secretKey, CryptoTradeMessage.class, CryptoQuoteMessage.class, CryptoBarMessage.class); } } From 4aab2d5f98adde2957f4bf3845426d20ff16d098 Mon Sep 17 00:00:00 2001 From: Jesse Thouin Date: Tue, 11 Jul 2023 11:39:41 -0400 Subject: [PATCH 43/84] Further updates to match v1beta3 crypto APIs. --- .../bar/latest_crypto_bars_response.json | 10 ++ .../orderbook/crypto_orderbook.json | 21 +++ .../orderbook/crypto_orderbook_entry.json | 16 ++ .../latest_crypto_orderbooks_response.json | 10 ++ .../quote/latest_crypto_quote_response.json | 14 -- .../quote/latest_crypto_quotes_response.json | 10 ++ .../historical/snapshot/crypto_snapshot.json | 31 ++++ .../snapshot/crypto_snapshots_response.json | 10 ++ .../trade/latest_crypto_trade_response.json | 14 -- .../trade/latest_crypto_trades_response.json | 10 ++ .../crypto/CryptoMarketDataEndpoint.java | 147 +++++++++++++++--- 11 files changed, 247 insertions(+), 46 deletions(-) create mode 100644 schema_json/endpoint/market_data/crypto/historical/bar/latest_crypto_bars_response.json create mode 100644 schema_json/endpoint/market_data/crypto/historical/orderbook/crypto_orderbook.json create mode 100644 schema_json/endpoint/market_data/crypto/historical/orderbook/crypto_orderbook_entry.json create mode 100644 schema_json/endpoint/market_data/crypto/historical/orderbook/latest_crypto_orderbooks_response.json delete mode 100644 schema_json/endpoint/market_data/crypto/historical/quote/latest_crypto_quote_response.json create mode 100644 schema_json/endpoint/market_data/crypto/historical/quote/latest_crypto_quotes_response.json create mode 100644 schema_json/endpoint/market_data/crypto/historical/snapshot/crypto_snapshot.json create mode 100644 schema_json/endpoint/market_data/crypto/historical/snapshot/crypto_snapshots_response.json delete mode 100644 schema_json/endpoint/market_data/crypto/historical/trade/latest_crypto_trade_response.json create mode 100644 schema_json/endpoint/market_data/crypto/historical/trade/latest_crypto_trades_response.json diff --git a/schema_json/endpoint/market_data/crypto/historical/bar/latest_crypto_bars_response.json b/schema_json/endpoint/market_data/crypto/historical/bar/latest_crypto_bars_response.json new file mode 100644 index 00000000..b502ddc3 --- /dev/null +++ b/schema_json/endpoint/market_data/crypto/historical/bar/latest_crypto_bars_response.json @@ -0,0 +1,10 @@ +{ + "type": "object", + "title": "See Historical Crypto Data.", + "properties": { + "bars": { + "existingJavaType": "java.util.Map", + "title": "The {@link java.util.Map} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.bar.CryptoBar}s." + } + } +} diff --git a/schema_json/endpoint/market_data/crypto/historical/orderbook/crypto_orderbook.json b/schema_json/endpoint/market_data/crypto/historical/orderbook/crypto_orderbook.json new file mode 100644 index 00000000..04409010 --- /dev/null +++ b/schema_json/endpoint/market_data/crypto/historical/orderbook/crypto_orderbook.json @@ -0,0 +1,21 @@ +{ + "type": "object", + "title": "See Latest Orderbook.", + "properties": { + "t": { + "existingJavaType": "java.time.ZonedDateTime", + "javaName": "timestamp", + "title": "Timestamp in RFC-3339 format with nanosecond precision." + }, + "b": { + "existingJavaType": "java.util.ArrayList", + "javaName": "bidSide", + "title": "An {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.orderbook.CryptoOrderbookEntry}s for the buy/bid side." + }, + "a": { + "existingJavaType": "java.util.ArrayList", + "javaName": "askSide", + "title": "An {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.orderbook.CryptoOrderbookEntry}s for the ask/sell side." + } + } +} diff --git a/schema_json/endpoint/market_data/crypto/historical/orderbook/crypto_orderbook_entry.json b/schema_json/endpoint/market_data/crypto/historical/orderbook/crypto_orderbook_entry.json new file mode 100644 index 00000000..5387ecc7 --- /dev/null +++ b/schema_json/endpoint/market_data/crypto/historical/orderbook/crypto_orderbook_entry.json @@ -0,0 +1,16 @@ +{ + "type": "object", + "title": "See Latest Orderbook.", + "properties": { + "p": { + "existingJavaType": "java.lang.Double", + "javaName": "price", + "title": "Price." + }, + "s": { + "existingJavaType": "java.lang.Double", + "javaName": "size", + "title": "Size." + } + } +} diff --git a/schema_json/endpoint/market_data/crypto/historical/orderbook/latest_crypto_orderbooks_response.json b/schema_json/endpoint/market_data/crypto/historical/orderbook/latest_crypto_orderbooks_response.json new file mode 100644 index 00000000..31c605d3 --- /dev/null +++ b/schema_json/endpoint/market_data/crypto/historical/orderbook/latest_crypto_orderbooks_response.json @@ -0,0 +1,10 @@ +{ + "type": "object", + "title": "See Historical Crypto Data.", + "properties": { + "orderbooks": { + "existingJavaType": "java.util.Map", + "title": "The {@link java.util.Map} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.orderbook.CryptoOrderbook}s." + } + } +} diff --git a/schema_json/endpoint/market_data/crypto/historical/quote/latest_crypto_quote_response.json b/schema_json/endpoint/market_data/crypto/historical/quote/latest_crypto_quote_response.json deleted file mode 100644 index 6db62c25..00000000 --- a/schema_json/endpoint/market_data/crypto/historical/quote/latest_crypto_quote_response.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "object", - "title": "See Historical Crypto Data.", - "properties": { - "symbol": { - "existingJavaType": "java.lang.String", - "title": "Symbol that was queried." - }, - "quote": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.CryptoQuote", - "title": "The latest {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.CryptoQuote}." - } - } -} diff --git a/schema_json/endpoint/market_data/crypto/historical/quote/latest_crypto_quotes_response.json b/schema_json/endpoint/market_data/crypto/historical/quote/latest_crypto_quotes_response.json new file mode 100644 index 00000000..995ccd02 --- /dev/null +++ b/schema_json/endpoint/market_data/crypto/historical/quote/latest_crypto_quotes_response.json @@ -0,0 +1,10 @@ +{ + "type": "object", + "title": "See Historical Crypto Data.", + "properties": { + "quotes": { + "existingJavaType": "java.util.Map", + "title": "The {@link java.util.Map} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.CryptoQuote}s." + } + } +} diff --git a/schema_json/endpoint/market_data/crypto/historical/snapshot/crypto_snapshot.json b/schema_json/endpoint/market_data/crypto/historical/snapshot/crypto_snapshot.json new file mode 100644 index 00000000..9ff6ab0d --- /dev/null +++ b/schema_json/endpoint/market_data/crypto/historical/snapshot/crypto_snapshot.json @@ -0,0 +1,31 @@ +{ + "type": "object", + "title": "See Snapshots.", + "properties": { + "dailyBar": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.bar.CryptoBar", + "javaName": "dailyBar", + "title": "OHLC aggregate of all the trades in a given interval." + }, + "latestQuote": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.CryptoQuote", + "javaName": "latestQuote", + "title": "The best bid and ask information for a given security." + }, + "latestTrade": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.CryptoTrade", + "javaName": "latestTrade", + "title": "A crypto trade" + }, + "minuteBar": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.bar.CryptoBar", + "javaName": "minuteBar", + "title": "OHLC aggregate of all the trades in a given interval." + }, + "prevDailyBar": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.bar.CryptoBar", + "javaName": "prevDailyBar", + "title": "OHLC aggregate of all the trades in a given interval." + } + } +} diff --git a/schema_json/endpoint/market_data/crypto/historical/snapshot/crypto_snapshots_response.json b/schema_json/endpoint/market_data/crypto/historical/snapshot/crypto_snapshots_response.json new file mode 100644 index 00000000..3ec3f3b8 --- /dev/null +++ b/schema_json/endpoint/market_data/crypto/historical/snapshot/crypto_snapshots_response.json @@ -0,0 +1,10 @@ +{ + "type": "object", + "title": "See Historical Crypto Data.", + "properties": { + "snapshots": { + "existingJavaType": "java.util.Map", + "title": "The {@link java.util.Map} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.snapshot.CryptoSnapshot}s." + } + } +} diff --git a/schema_json/endpoint/market_data/crypto/historical/trade/latest_crypto_trade_response.json b/schema_json/endpoint/market_data/crypto/historical/trade/latest_crypto_trade_response.json deleted file mode 100644 index b49e4ef7..00000000 --- a/schema_json/endpoint/market_data/crypto/historical/trade/latest_crypto_trade_response.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "object", - "title": "See Historical Crypto Data.", - "properties": { - "symbol": { - "existingJavaType": "java.lang.String", - "title": "Symbol that was queried." - }, - "trade": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.CryptoTrade", - "title": "The latest {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.CryptoTrade}." - } - } -} diff --git a/schema_json/endpoint/market_data/crypto/historical/trade/latest_crypto_trades_response.json b/schema_json/endpoint/market_data/crypto/historical/trade/latest_crypto_trades_response.json new file mode 100644 index 00000000..4e6749c2 --- /dev/null +++ b/schema_json/endpoint/market_data/crypto/historical/trade/latest_crypto_trades_response.json @@ -0,0 +1,10 @@ +{ + "type": "object", + "title": "See Historical Crypto Data.", + "properties": { + "trades": { + "existingJavaType": "java.util.Map", + "title": "The {@link java.util.Map} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.CryptoTrade}s." + } + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/crypto/CryptoMarketDataEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/crypto/CryptoMarketDataEndpoint.java index 0062d3fe..bebe7a87 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/crypto/CryptoMarketDataEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/crypto/CryptoMarketDataEndpoint.java @@ -3,12 +3,17 @@ import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.historical.bar.enums.BarTimePeriod; import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.bar.CryptoBar; import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.bar.CryptoBarsResponse; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.bar.LatestCryptoBarsResponse; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.orderbook.CryptoOrderbook; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.orderbook.LatestCryptoOrderbooksResponse; import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.CryptoQuote; import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.CryptoQuotesResponse; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.LatestCryptoQuoteResponse; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.LatestCryptoQuotesResponse; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.snapshot.CryptoSnapshot; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.snapshot.CryptoSnapshotsResponse; import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.CryptoTrade; import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.CryptoTradesResponse; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.LatestCryptoTradeResponse; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.LatestCryptoTradesResponse; import net.jacobpeterson.alpaca.rest.AlpacaClient; import net.jacobpeterson.alpaca.rest.AlpacaClientException; import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; @@ -18,7 +23,6 @@ import java.time.ZonedDateTime; import java.util.Collection; -import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkNotNull; @@ -40,7 +44,7 @@ public CryptoMarketDataEndpoint(AlpacaClient alpacaClient) { } /** - * Gets {@link CryptoTrade} historical data for the requested crypto symbol. + * Gets {@link CryptoTrade} historical data for the requested crypto symbols. * * @param symbols a {@link Collection} of symbols to query for * @param start filter data equal to or after this {@link ZonedDateTime}. Fractions of a second are not @@ -58,6 +62,9 @@ public CryptoMarketDataEndpoint(AlpacaClient alpacaClient) { public CryptoTradesResponse getTrades(Collection symbols, ZonedDateTime start, ZonedDateTime end, Integer limit, String pageToken) throws AlpacaClientException { checkNotNull(symbols); + if (symbols.isEmpty()) { + throw new AlpacaClientException("Collection symbols must not be empty."); + } HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() .addPathSegment(endpointPathSegment) @@ -89,33 +96,65 @@ public CryptoTradesResponse getTrades(Collection symbols, ZonedDateTime } /** - * Gets the latest {@link CryptoTrade} for the requested security. + * Gets the latest {@link CryptoTrade}s for the requested securities. + * + * @param symbols a {@link Collection} of symbols to query for + * + * @return the {@link LatestCryptoTradesResponse} + * + * @throws AlpacaClientException thrown for {@link AlpacaClientException}s + */ + public LatestCryptoTradesResponse getLatestTrades(Collection symbols) throws AlpacaClientException { + checkNotNull(symbols); + if (symbols.isEmpty()) { + throw new AlpacaClientException("Collection symbols must not be empty."); + } + + HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() + .addPathSegment(endpointPathSegment) + .addPathSegment(LOCALE) + .addPathSegment("latest") + .addPathSegment("trades"); + + urlBuilder.addQueryParameter("symbols", String.join(",", symbols)); + + Request request = alpacaClient.requestBuilder(urlBuilder.build()) + .get() + .build(); + return alpacaClient.requestObject(request, LatestCryptoTradesResponse.class); + } + + /** + * Gets the latest {@link CryptoBar}s for the requested securities. * * @param symbols a {@link Collection} of symbols to query for * - * @return the {@link LatestCryptoTradeResponse} + * @return the {@link LatestCryptoBarsResponse} * * @throws AlpacaClientException thrown for {@link AlpacaClientException}s */ - public LatestCryptoTradeResponse getLatestTrade(Collection symbols) throws AlpacaClientException { + public LatestCryptoBarsResponse getLatestBars(Collection symbols) throws AlpacaClientException { checkNotNull(symbols); + if (symbols.isEmpty()) { + throw new AlpacaClientException("Collection symbols must not be empty."); + } HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() .addPathSegment(endpointPathSegment) .addPathSegment(LOCALE) - .addPathSegment("trades") - .addPathSegment("latest"); + .addPathSegment("latest") + .addPathSegment("bars"); urlBuilder.addQueryParameter("symbols", String.join(",", symbols)); Request request = alpacaClient.requestBuilder(urlBuilder.build()) .get() .build(); - return alpacaClient.requestObject(request, LatestCryptoTradeResponse.class); + return alpacaClient.requestObject(request, LatestCryptoBarsResponse.class); } /** - * Gets {@link CryptoQuote} historical data for the requested crypto symbol. + * Gets {@link CryptoQuote} historical data for the requested crypto symbols. * * @param symbols a {@link Collection} of symbols to query for * @param start filter data equal to or after this {@link ZonedDateTime}. Fractions of a second are not @@ -133,6 +172,9 @@ public LatestCryptoTradeResponse getLatestTrade(Collection symbols) thro public CryptoQuotesResponse getQuotes(Collection symbols, ZonedDateTime start, ZonedDateTime end, Integer limit, String pageToken) throws AlpacaClientException { checkNotNull(symbols); + if (symbols.isEmpty()) { + throw new AlpacaClientException("Collection symbols must not be empty."); + } HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() .addPathSegment(endpointPathSegment) @@ -164,29 +206,89 @@ public CryptoQuotesResponse getQuotes(Collection symbols, ZonedDateTime } /** - * Gets the latest {@link CryptoQuote} for the requested security. + * Gets the latest {@link CryptoQuote}s for the requested securities. + * + * @param symbols a {@link Collection} of symbols to query for + * + * @return the {@link LatestCryptoQuotesResponse} + * + * @throws AlpacaClientException thrown for {@link AlpacaClientException}s + */ + public LatestCryptoQuotesResponse getLatestQuotes(Collection symbols) throws AlpacaClientException { + checkNotNull(symbols); + if (symbols.isEmpty()) { + throw new AlpacaClientException("Collection symbols must not be empty."); + } + + HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() + .addPathSegment(endpointPathSegment) + .addPathSegment(LOCALE) + .addPathSegment("latest") + .addPathSegment("quotes"); + + urlBuilder.addQueryParameter("symbols", String.join(",", symbols)); + + Request request = alpacaClient.requestBuilder(urlBuilder.build()) + .get() + .build(); + return alpacaClient.requestObject(request, LatestCryptoQuotesResponse.class); + } + + /** + * Gets the latest {@link CryptoSnapshot} for the requested securities. + * + * @param symbols a {@link Collection} of symbols to query for + * + * @return the {@link CryptoSnapshotsResponse} + * + * @throws AlpacaClientException thrown for {@link AlpacaClientException}s + */ + public CryptoSnapshotsResponse getSnapshots(Collection symbols) throws AlpacaClientException { + checkNotNull(symbols); + if (symbols.isEmpty()) { + throw new AlpacaClientException("Collection symbols must not be empty."); + } + + HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() + .addPathSegment(endpointPathSegment) + .addPathSegment(LOCALE) + .addPathSegment("snapshots"); + + urlBuilder.addQueryParameter("symbols", String.join(",", symbols)); + + Request request = alpacaClient.requestBuilder(urlBuilder.build()) + .get() + .build(); + return alpacaClient.requestObject(request, CryptoSnapshotsResponse.class); + } + + /** + * Gets the latest {@link CryptoOrderbook}s for the requested securities. * * @param symbols a {@link Collection} of symbols to query for * - * @return the {@link LatestCryptoQuoteResponse} + * @return the {@link LatestCryptoOrderbooksResponse} * * @throws AlpacaClientException thrown for {@link AlpacaClientException}s */ - public LatestCryptoQuoteResponse getLatestQuote(Collection symbols) throws AlpacaClientException { + public LatestCryptoOrderbooksResponse getLatestOrderbooks(Collection symbols) throws AlpacaClientException { checkNotNull(symbols); + if (symbols.isEmpty()) { + throw new AlpacaClientException("Collection symbols must not be empty."); + } HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() .addPathSegment(endpointPathSegment) .addPathSegment(LOCALE) - .addPathSegment("quotes") - .addPathSegment("latest"); + .addPathSegment("latest") + .addPathSegment("orderbooks"); urlBuilder.addQueryParameter("symbols", String.join(",", symbols)); Request request = alpacaClient.requestBuilder(urlBuilder.build()) .get() .build(); - return alpacaClient.requestObject(request, LatestCryptoQuoteResponse.class); + return alpacaClient.requestObject(request, LatestCryptoOrderbooksResponse.class); } /** @@ -195,6 +297,8 @@ public LatestCryptoQuoteResponse getLatestQuote(Collection symbols) thro * @param symbols a {@link Collection} of symbols to query for * @param start filter data equal to or after this {@link ZonedDateTime}. Fractions of a second are * not accepted. null for now. + * @param end filter data equal to or before this {@link ZonedDateTime}. Fractions of a second are + * not accepted. null for now. * @param limit number of data points to return. Must be in range 1-10000, defaults to 1000 if * null is given * @param pageToken pagination token to continue from @@ -209,9 +313,12 @@ public LatestCryptoQuoteResponse getLatestQuote(Collection symbols) thro * * @throws AlpacaClientException thrown for {@link AlpacaClientException}s */ - public CryptoBarsResponse getBars(Collection symbols, ZonedDateTime start, Integer limit, + public CryptoBarsResponse getBars(Collection symbols, ZonedDateTime start, ZonedDateTime end, Integer limit, String pageToken, int barTimePeriodDuration, BarTimePeriod barTimePeriod) throws AlpacaClientException { checkNotNull(symbols); + if (symbols.isEmpty()) { + throw new AlpacaClientException("Collection symbols must not be empty."); + } checkNotNull(barTimePeriod); HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() @@ -225,6 +332,10 @@ public CryptoBarsResponse getBars(Collection symbols, ZonedDateTime star urlBuilder.addQueryParameter("start", FormatUtil.toRFC3339Format(start)); } + if (end != null) { + urlBuilder.addQueryParameter("end", FormatUtil.toRFC3339Format(end)); + } + if (limit != null) { urlBuilder.addQueryParameter("limit", limit.toString()); } From eb7f551f547bbaf6015797b2972aa2bed1fdb50c Mon Sep 17 00:00:00 2001 From: Jesse Thouin Date: Thu, 13 Jul 2023 16:54:02 -0400 Subject: [PATCH 44/84] Removed getQuotes as it doesn't have a matching API call with Alpaca. --- .../quote/crypto_quotes_response.json | 14 ----- .../crypto/CryptoMarketDataEndpoint.java | 53 ------------------- 2 files changed, 67 deletions(-) delete mode 100644 schema_json/endpoint/market_data/crypto/historical/quote/crypto_quotes_response.json diff --git a/schema_json/endpoint/market_data/crypto/historical/quote/crypto_quotes_response.json b/schema_json/endpoint/market_data/crypto/historical/quote/crypto_quotes_response.json deleted file mode 100644 index a5010f8c..00000000 --- a/schema_json/endpoint/market_data/crypto/historical/quote/crypto_quotes_response.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "object", - "title": "See Historical Crypto Data.", - "properties": { - "quotes": { - "existingJavaType": "java.util.Map>", - "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.CryptoQuote}s." - }, - "next_page_token": { - "existingJavaType": "java.lang.String", - "title": "Token that can be used to query the next page." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/crypto/CryptoMarketDataEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/crypto/CryptoMarketDataEndpoint.java index bebe7a87..c1a8947f 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/crypto/CryptoMarketDataEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/crypto/CryptoMarketDataEndpoint.java @@ -7,7 +7,6 @@ import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.orderbook.CryptoOrderbook; import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.orderbook.LatestCryptoOrderbooksResponse; import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.CryptoQuote; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.CryptoQuotesResponse; import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.LatestCryptoQuotesResponse; import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.snapshot.CryptoSnapshot; import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.snapshot.CryptoSnapshotsResponse; @@ -153,58 +152,6 @@ public LatestCryptoBarsResponse getLatestBars(Collection symbols) throws return alpacaClient.requestObject(request, LatestCryptoBarsResponse.class); } - /** - * Gets {@link CryptoQuote} historical data for the requested crypto symbols. - * - * @param symbols a {@link Collection} of symbols to query for - * @param start filter data equal to or after this {@link ZonedDateTime}. Fractions of a second are not - * accepted. null for the current day in Central Time. - * @param end filter data equal to or before this {@link ZonedDateTime}. Fractions of a second are not - * accepted. null for now. - * @param limit number of data points to return. Must be in range 1-10000, defaults to 1000 if null - * is given - * @param pageToken pagination token to continue from - * - * @return the {@link CryptoQuotesResponse} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public CryptoQuotesResponse getQuotes(Collection symbols, ZonedDateTime start, - ZonedDateTime end, Integer limit, String pageToken) throws AlpacaClientException { - checkNotNull(symbols); - if (symbols.isEmpty()) { - throw new AlpacaClientException("Collection symbols must not be empty."); - } - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment) - .addPathSegment(LOCALE) - .addPathSegment("quotes"); - - urlBuilder.addQueryParameter("symbols", String.join(",", symbols)); - - if (start != null) { - urlBuilder.addQueryParameter("start", FormatUtil.toRFC3339Format(start)); - } - - if (end != null) { - urlBuilder.addQueryParameter("end", FormatUtil.toRFC3339Format(end)); - } - - if (limit != null) { - urlBuilder.addQueryParameter("limit", limit.toString()); - } - - if (pageToken != null) { - urlBuilder.addQueryParameter("page_token", pageToken); - } - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, CryptoQuotesResponse.class); - } - /** * Gets the latest {@link CryptoQuote}s for the requested securities. * From 6fdb6fcab02b68a0c54eeeabaa95c5d651b75ce7 Mon Sep 17 00:00:00 2001 From: Jesse Thouin Date: Sat, 15 Jul 2023 12:50:29 -0400 Subject: [PATCH 45/84] Alpaca removed OrderTimeInForce.DAY as a valid time in force for crypto orders. --- .../alpaca/rest/endpoint/orders/OrdersEndpoint.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/orders/OrdersEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/orders/OrdersEndpoint.java index 3212bf75..d2d355dd 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/orders/OrdersEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/orders/OrdersEndpoint.java @@ -242,7 +242,7 @@ public Order requestMarketOrder(String symbol, Integer quantity, OrderSide side, * most likely method of filling an order. Market orders fill nearly instantaneously. This method calls {@link * #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, Double, * Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#MARKET} and {@link - * OrderTimeInForce#DAY} with a fractional quantity so check the Javadoc for that method for parameter details. + * OrderTimeInForce#GOOD_UNTIL_CANCELLED} with a fractional quantity so check the Javadoc for that method for parameter details. * * @return the requested {@link Order} * @@ -250,7 +250,7 @@ public Order requestMarketOrder(String symbol, Integer quantity, OrderSide side, */ public Order requestFractionalMarketOrder(String symbol, Double quantity, OrderSide side) throws AlpacaClientException { - return requestOrder(symbol, quantity, null, side, OrderType.MARKET, OrderTimeInForce.DAY, null, null, null, + return requestOrder(symbol, quantity, null, side, OrderType.MARKET, OrderTimeInForce.GOOD_UNTIL_CANCELLED, null, null, null, null, null, null, null, null, null, null); } @@ -259,7 +259,7 @@ public Order requestFractionalMarketOrder(String symbol, Double quantity, OrderS * most likely method of filling an order. Market orders fill nearly instantaneously. This method calls {@link * #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, Double, * Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#MARKET} and {@link - * OrderTimeInForce#DAY} with a notional dollar amount so check the Javadoc for that method for the parameter + * OrderTimeInForce#GOOD_UNTIL_CANCELLED} with a notional dollar amount so check the Javadoc for that method for the parameter * details. * * @return the requested {@link Order} @@ -268,7 +268,7 @@ public Order requestFractionalMarketOrder(String symbol, Double quantity, OrderS */ public Order requestNotionalMarketOrder(String symbol, Double notional, OrderSide side) throws AlpacaClientException { - return requestOrder(symbol, null, notional, side, OrderType.MARKET, OrderTimeInForce.DAY, null, null, null, + return requestOrder(symbol, null, notional, side, OrderType.MARKET, OrderTimeInForce.GOOD_UNTIL_CANCELLED, null, null, null, null, null, null, null, null, null, null); } From 9ca1bcd9dec7f7fab6dd58da1ab4bc8d3722dd73 Mon Sep 17 00:00:00 2001 From: Daniel Mejer Date: Mon, 7 Aug 2023 18:13:31 +0200 Subject: [PATCH 46/84] Add builder to AlpacaAPI --- README.md | 6 +++ .../net/jacobpeterson/alpaca/AlpacaAPI.java | 53 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/README.md b/README.md index a29ffc56..aff83e53 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,12 @@ AlpacaAPI alpacaAPI = new AlpacaAPI(keyID, secretKey, EndpointAPIType.LIVE, Data // This constructor is for OAuth tokens String oAuthToken = ""; AlpacaAPI alpacaAPI = new AlpacaAPI(oAuthToken); + +// The following approach uses builder pattern. Attributes not provided explicitly are retrieved from 'alpaca.properties' file +AlpacaAPI alpacaAPI = AlpacaAPI.builder() + .withEndpointAPIType(EndpointAPIType.PAPER) + .build(); + ``` Note that this library uses [OkHttp](https://square.github.io/okhttp/) as its HTTP client library which creates background threads to service requests. These threads persist even if the main thread exists so if you want to destroy these threads when you're done using [`AlpacaAPI`](src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java) so your program can exit without calling `System.exit()`, use the following snippet: diff --git a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java index 63980318..60b47293 100644 --- a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java +++ b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java @@ -78,6 +78,17 @@ public AlpacaAPI() { AlpacaProperties.DATA_API_TYPE); } + /** + * Instantiates a new {@link AlpacaAPI} using properties specified in Builder, otherwise from + * alpaca.properties file (or their associated defaults). + */ + private AlpacaAPI(Builder builder) { + this(builder.keyID, + builder.secretKey, + builder.endpointAPIType, + builder.dataAPIType); + } + /** * Instantiates a new {@link AlpacaAPI}. * @@ -308,4 +319,46 @@ public AlpacaClient getCryptoDataClient() { public AlpacaClient getStockDataClient() { return stockDataClient; } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String keyID; + private String secretKey; + private EndpointAPIType endpointAPIType; + private DataAPIType dataAPIType; + + private Builder() { + this.keyID = AlpacaProperties.KEY_ID; + this.secretKey = AlpacaProperties.SECRET_KEY; + this.endpointAPIType = AlpacaProperties.ENDPOINT_API_TYPE; + this.dataAPIType = AlpacaProperties.DATA_API_TYPE; + } + + public Builder withKeyID(String val) { + keyID = val; + return this; + } + + public Builder withSecretKey(String val) { + secretKey = val; + return this; + } + + public Builder withEndpointAPIType(EndpointAPIType val) { + endpointAPIType = val; + return this; + } + + public Builder withDataAPIType(DataAPIType val) { + dataAPIType = val; + return this; + } + + public AlpacaAPI build() { + return new AlpacaAPI(this); + } + } } From 854a5ddf55af940d406b60d0828986984e52ca56 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Sun, 20 Aug 2023 16:19:10 -0600 Subject: [PATCH 47/84] Minor documentation updates --- README.md | 2 +- build.gradle | 1 + .../net/jacobpeterson/alpaca/AlpacaAPI.java | 23 +++++++++++++------ 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index aff83e53..a6c51d49 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ AlpacaAPI alpacaAPI = new AlpacaAPI(keyID, secretKey, EndpointAPIType.LIVE, Data String oAuthToken = ""; AlpacaAPI alpacaAPI = new AlpacaAPI(oAuthToken); -// The following approach uses builder pattern. Attributes not provided explicitly are retrieved from 'alpaca.properties' file +// This approach uses a builder pattern. Attributes not provided explicitly are retrieved from 'alpaca.properties' file AlpacaAPI alpacaAPI = AlpacaAPI.builder() .withEndpointAPIType(EndpointAPIType.PAPER) .build(); diff --git a/build.gradle b/build.gradle index 588393d3..a40691fa 100644 --- a/build.gradle +++ b/build.gradle @@ -237,6 +237,7 @@ jsonSchema2Pojo { annotationStyle = 'gson' sourceType = 'jsonschema' customDateTimePattern = "yyyy-MM-ddTHH:mm:ssZ" + includeGeneratedAnnotation = false includeConstructors = true serializable = true includeGetters = true diff --git a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java index 60b47293..c043b78f 100644 --- a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java +++ b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java @@ -5,19 +5,19 @@ import net.jacobpeterson.alpaca.model.properties.EndpointAPIType; import net.jacobpeterson.alpaca.properties.AlpacaProperties; import net.jacobpeterson.alpaca.rest.AlpacaClient; +import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; +import net.jacobpeterson.alpaca.rest.endpoint.account.AccountEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.accountactivities.AccountActivitiesEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.accountconfiguration.AccountConfigurationEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.account.AccountEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.assets.AssetsEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.calendar.CalendarEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.clock.ClockEndpoint; +import net.jacobpeterson.alpaca.rest.endpoint.marketdata.crypto.CryptoMarketDataEndpoint; +import net.jacobpeterson.alpaca.rest.endpoint.marketdata.stock.StockMarketDataEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.orders.OrdersEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.portfoliohistory.PortfolioHistoryEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.positions.PositionsEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.watchlist.WatchlistEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.marketdata.crypto.CryptoMarketDataEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.marketdata.stock.StockMarketDataEndpoint; import net.jacobpeterson.alpaca.websocket.AlpacaWebsocket; import net.jacobpeterson.alpaca.websocket.marketdata.MarketDataWebsocketInterface; import net.jacobpeterson.alpaca.websocket.marketdata.crypto.CryptoMarketDataWebsocket; @@ -32,8 +32,8 @@ import static com.google.common.base.Preconditions.checkNotNull; /** - * The {@link AlpacaAPI} class contains several instances of various {@link AlpacaEndpoint}s and {@link - * AlpacaWebsocket}s to interface with Alpaca. You will generally only need one instance of this class in your + * The {@link AlpacaAPI} class contains several instances of various {@link AlpacaEndpoint}s and + * {@link AlpacaWebsocket}s to interface with Alpaca. You will generally only need one instance of this class in your * application. Note that many methods inside the various {@link AlpacaEndpoint}s allow null to be passed * in as a parameter if it is optional. * @@ -79,7 +79,7 @@ public AlpacaAPI() { } /** - * Instantiates a new {@link AlpacaAPI} using properties specified in Builder, otherwise from + * Instantiates a new {@link AlpacaAPI} using properties specified in the given {@link Builder}, otherwise from * alpaca.properties file (or their associated defaults). */ private AlpacaAPI(Builder builder) { @@ -320,11 +320,20 @@ public AlpacaClient getStockDataClient() { return stockDataClient; } + /** + * Creates a {@link Builder} for {@link AlpacaAPI}. + * + * @return a {@link Builder} + */ public static Builder builder() { return new Builder(); } + /** + * A builder for {@link AlpacaAPI} + */ public static final class Builder { + private String keyID; private String secretKey; private EndpointAPIType endpointAPIType; From 9d253ff4f5acf7d2ff65cbfe903720e19299fd1d Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Sun, 20 Aug 2023 16:33:26 -0600 Subject: [PATCH 48/84] Auto-format --- .../net/jacobpeterson/alpaca/AlpacaAPI.java | 12 +- .../alpaca/rest/AlpacaClient.java | 16 +- .../crypto/CryptoMarketDataEndpoint.java | 15 +- .../stock/StockMarketDataEndpoint.java | 16 +- .../rest/endpoint/orders/OrdersEndpoint.java | 154 +++++++++--------- .../marketdata/MarketDataWebsocket.java | 8 +- .../streaming/StreamingWebsocket.java | 4 +- src/main/resources/alpaca.default.properties | 1 - .../alpaca/test/live/AlpacaAPITest.java | 9 +- 9 files changed, 120 insertions(+), 115 deletions(-) diff --git a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java index c71f496b..6ae47c47 100644 --- a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java +++ b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java @@ -5,19 +5,19 @@ import net.jacobpeterson.alpaca.model.properties.EndpointAPIType; import net.jacobpeterson.alpaca.properties.AlpacaProperties; import net.jacobpeterson.alpaca.rest.AlpacaClient; +import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; +import net.jacobpeterson.alpaca.rest.endpoint.account.AccountEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.accountactivities.AccountActivitiesEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.accountconfiguration.AccountConfigurationEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.account.AccountEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.assets.AssetsEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.calendar.CalendarEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.clock.ClockEndpoint; +import net.jacobpeterson.alpaca.rest.endpoint.marketdata.crypto.CryptoMarketDataEndpoint; +import net.jacobpeterson.alpaca.rest.endpoint.marketdata.stock.StockMarketDataEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.orders.OrdersEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.portfoliohistory.PortfolioHistoryEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.positions.PositionsEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.watchlist.WatchlistEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.marketdata.crypto.CryptoMarketDataEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.marketdata.stock.StockMarketDataEndpoint; import net.jacobpeterson.alpaca.websocket.AlpacaWebsocket; import net.jacobpeterson.alpaca.websocket.marketdata.MarketDataWebsocketInterface; import net.jacobpeterson.alpaca.websocket.marketdata.crypto.CryptoMarketDataWebsocket; @@ -32,8 +32,8 @@ import static com.google.common.base.Preconditions.checkNotNull; /** - * The {@link AlpacaAPI} class contains several instances of various {@link AlpacaEndpoint}s and {@link - * AlpacaWebsocket}s to interface with Alpaca. You will generally only need one instance of this class in your + * The {@link AlpacaAPI} class contains several instances of various {@link AlpacaEndpoint}s and + * {@link AlpacaWebsocket}s to interface with Alpaca. You will generally only need one instance of this class in your * application. Note that many methods inside the various {@link AlpacaEndpoint}s allow null to be passed * in as a parameter if it is optional. * diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClient.java b/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClient.java index 7fe5cc27..dc11df3f 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClient.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClient.java @@ -127,8 +127,8 @@ public T requestObject(Request request, Type type) throws AlpacaClientExcept * * @param the type of object * @param request the {@link Request} - * @param isSuccessCode throws {@link AlpacaClientException} if passed in {@link Response#code()} to the {@link - * Predicate#test(Object)} returns false + * @param isSuccessCode throws {@link AlpacaClientException} if passed in {@link Response#code()} to the + * {@link Predicate#test(Object)} returns false * @param type the object {@link Type} * * @return the requested object @@ -151,8 +151,8 @@ public JsonElement requestJSON(Request request) throws AlpacaClientException { * Requests a {@link JsonElement} given a {@link Request}. * * @param request the {@link Request} - * @param isSuccessCode throws {@link AlpacaClientException} if passed in {@link Response#code()} to the {@link - * Predicate#test(Object)} returns false + * @param isSuccessCode throws {@link AlpacaClientException} if passed in {@link Response#code()} to the + * {@link Predicate#test(Object)} returns false * * @return the {@link JsonElement} * @@ -178,8 +178,8 @@ public JsonElement requestJSON(Request request, Predicate isSuccessCode * Sends a {@link Request} and ignores any {@link ResponseBody}. * * @param request the {@link Request} - * @param isSuccessCode throws {@link AlpacaClientException} if passed in {@link Response#code()} to the {@link - * Predicate#test(Object)} returns false + * @param isSuccessCode throws {@link AlpacaClientException} if passed in {@link Response#code()} to the + * {@link Predicate#test(Object)} returns false * * @throws AlpacaClientException thrown for {@link AlpacaClientException}s */ @@ -193,8 +193,8 @@ public void requestVoid(Request request, Predicate isSuccessCode) throw * Sends a {@link Request} and returns a {@link Response}. * * @param request the {@link Request} - * @param isSuccessCode throws {@link AlpacaClientException} if passed in {@link Response#code()} to the {@link - * Predicate#test(Object)} returns false + * @param isSuccessCode throws {@link AlpacaClientException} if passed in {@link Response#code()} to the + * {@link Predicate#test(Object)} returns false * * @return the {@link Response}. Be sure to call {@link Response#close()} after use. * diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/crypto/CryptoMarketDataEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/crypto/CryptoMarketDataEndpoint.java index c1a8947f..f429a32c 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/crypto/CryptoMarketDataEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/crypto/CryptoMarketDataEndpoint.java @@ -31,6 +31,7 @@ * Crypto Market Data API. */ public class CryptoMarketDataEndpoint extends AlpacaEndpoint { + public static final String LOCALE = "us"; /** @@ -97,7 +98,7 @@ public CryptoTradesResponse getTrades(Collection symbols, ZonedDateTime /** * Gets the latest {@link CryptoTrade}s for the requested securities. * - * @param symbols a {@link Collection} of symbols to query for + * @param symbols a {@link Collection} of symbols to query for * * @return the {@link LatestCryptoTradesResponse} * @@ -126,7 +127,7 @@ public LatestCryptoTradesResponse getLatestTrades(Collection symbols) th /** * Gets the latest {@link CryptoBar}s for the requested securities. * - * @param symbols a {@link Collection} of symbols to query for + * @param symbols a {@link Collection} of symbols to query for * * @return the {@link LatestCryptoBarsResponse} * @@ -155,7 +156,7 @@ public LatestCryptoBarsResponse getLatestBars(Collection symbols) throws /** * Gets the latest {@link CryptoQuote}s for the requested securities. * - * @param symbols a {@link Collection} of symbols to query for + * @param symbols a {@link Collection} of symbols to query for * * @return the {@link LatestCryptoQuotesResponse} * @@ -184,7 +185,7 @@ public LatestCryptoQuotesResponse getLatestQuotes(Collection symbols) th /** * Gets the latest {@link CryptoSnapshot} for the requested securities. * - * @param symbols a {@link Collection} of symbols to query for + * @param symbols a {@link Collection} of symbols to query for * * @return the {@link CryptoSnapshotsResponse} * @@ -212,7 +213,7 @@ public CryptoSnapshotsResponse getSnapshots(Collection symbols) throws A /** * Gets the latest {@link CryptoOrderbook}s for the requested securities. * - * @param symbols a {@link Collection} of symbols to query for + * @param symbols a {@link Collection} of symbols to query for * * @return the {@link LatestCryptoOrderbooksResponse} * @@ -252,8 +253,8 @@ public LatestCryptoOrderbooksResponse getLatestOrderbooks(Collection sym * @param barTimePeriodDuration the duration for the given barTimePeriod parameter. e.g. for * 15Min bars, you would supply 15 for this parameter and * {@link BarTimePeriod#MINUTE} for the barTimePeriod parameter. - * @param barTimePeriod the {@link BarTimePeriod} e.g. for 15Min bars, you would supply {@link - * BarTimePeriod#MINUTE} for this parameter and 15 for the + * @param barTimePeriod the {@link BarTimePeriod} e.g. for 15Min bars, you would supply + * {@link BarTimePeriod#MINUTE} for this parameter and 15 for the * barTimePeriodDuration parameter. * * @return the {@link CryptoBarsResponse} diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/stock/StockMarketDataEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/stock/StockMarketDataEndpoint.java index d47b167d..3111dde7 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/stock/StockMarketDataEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/stock/StockMarketDataEndpoint.java @@ -199,11 +199,11 @@ public LatestStockQuoteResponse getLatestQuote(String symbol) throws AlpacaClien * @param barTimePeriodDuration the duration for the given barTimePeriod parameter. e.g. for * 15Min bars, you would supply 15 for this parameter and * {@link BarTimePeriod#MINUTE} for the barTimePeriod parameter. - * @param barTimePeriod the {@link BarTimePeriod}. e.g. for 15Min bars, you would supply {@link - * BarTimePeriod#MINUTE} for this parameter and 15 for the + * @param barTimePeriod the {@link BarTimePeriod}. e.g. for 15Min bars, you would supply + * {@link BarTimePeriod#MINUTE} for this parameter and 15 for the * barTimePeriodDuration parameter. - * @param barAdjustment specifies the corporate action adjustment for the stocks. Default value is {@link - * BarAdjustment#RAW} + * @param barAdjustment specifies the corporate action adjustment for the stocks. Default value is + * {@link BarAdjustment#RAW} * @param barFeed defaults to {@link BarFeed#IEX} for Free users and {@link BarFeed#SIP} for users * with an Unlimited subscription * @@ -265,11 +265,11 @@ public StockBarsResponse getBars(String symbol, ZonedDateTime start, ZonedDateTi * @param barTimePeriodDuration the duration for the given barTimePeriod parameter. e.g. for * 15Min bars, you would supply 15 for this parameter and * {@link BarTimePeriod#MINUTE} for the barTimePeriod parameter. - * @param barTimePeriod the {@link BarTimePeriod}. e.g. for 15Min bars, you would supply {@link - * BarTimePeriod#MINUTE} for this parameter and 15 for the + * @param barTimePeriod the {@link BarTimePeriod}. e.g. for 15Min bars, you would supply + * {@link BarTimePeriod#MINUTE} for this parameter and 15 for the * barTimePeriodDuration parameter. - * @param barAdjustment specifies the corporate action adjustment for the stocks. Default value is {@link - * BarAdjustment#RAW} + * @param barAdjustment specifies the corporate action adjustment for the stocks. Default value is + * {@link BarAdjustment#RAW} * @param barFeed defaults to {@link BarFeed#IEX} for Free users and {@link BarFeed#SIP} for users * with an Unlimited subscription * diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/orders/OrdersEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/orders/OrdersEndpoint.java index d2d355dd..0ad4a66e 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/orders/OrdersEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/orders/OrdersEndpoint.java @@ -56,8 +56,8 @@ public OrdersEndpoint(AlpacaClient alpacaClient) { * @param limit the maximum number of orders in response. Defaults to 50 and max is 500. * @param after the response will include only ones submitted after this timestamp (exclusive) * @param until the response will include only ones submitted until this timestamp (exclusive) - * @param direction the chronological order of response based on the submission time. Defaults to {@link - * SortDirection#DESCENDING}. + * @param direction the chronological order of response based on the submission time. Defaults to + * {@link SortDirection#DESCENDING}. * @param nested if true, the result will roll up multi-leg orders under the legs field of primary order. * @param symbols a {@link Collection} of symbols to filter by (e.g. "AAPL,TSLA,MSFT"). A currency pair is * required for crypto orders (e.g. "BTCUSD,BCHUSD,LTCUSD,ETCUSD"). null for no @@ -120,19 +120,19 @@ public List get(CurrentOrderStatus status, Integer limit, ZonedDateTime a * @param timeInForce the {@link OrderTimeInForce} * @param limitPrice required if type is {@link OrderType#LIMIT} or {@link OrderType#STOP_LIMIT} * @param stopPrice required if type is {@link OrderType#STOP} or {@link OrderType#STOP_LIMIT} - * @param trailPrice this or trail_percent is required if type is {@link - * OrderType#TRAILING_STOP} - * @param trailPercent this or trail_price is required if type is {@link - * OrderType#TRAILING_STOP} + * @param trailPrice this or trail_percent is required if type is + * {@link OrderType#TRAILING_STOP} + * @param trailPercent this or trail_price is required if type is + * {@link OrderType#TRAILING_STOP} * @param extendedHours (default) false. If true, order will be eligible to execute in premarket/afterhours. * Only works with type {@link OrderType#LIMIT} and {@link OrderTimeInForce#DAY}. * @param clientOrderId a unique identifier for the order. Automatically generated if null. * @param orderClass the {@link OrderClass}. For details of non-simple order classes, please see "Bracket * Order Overview" on the Alpaca Docs. - * @param takeProfitLimitPrice additional parameter for take-profit leg of advanced orders. Required for {@link - * OrderClass#BRACKET}. - * @param stopLossStopPrice additional parameters for stop-loss leg of advanced orders. Required for {@link - * OrderClass#BRACKET}. + * @param takeProfitLimitPrice additional parameter for take-profit leg of advanced orders. Required for + * {@link OrderClass#BRACKET}. + * @param stopLossStopPrice additional parameters for stop-loss leg of advanced orders. Required for + * {@link OrderClass#BRACKET}. * @param stopLossLimitPrice additional parameters for stop-loss leg of advanced orders. The stop-loss order * becomes a stop-limit order if specified. * @@ -222,10 +222,10 @@ public Order requestOrder(String symbol, Double quantity, Double notional, Order /** * A market order is a request to buy or sell a security at the currently available market price. It provides the - * most likely method of filling an order. Market orders fill nearly instantaneously. This method calls {@link - * #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, Double, - * Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#MARKET} so check the Javadoc for that - * method for parameter details. + * most likely method of filling an order. Market orders fill nearly instantaneously. This method calls + * {@link #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, + * Double, Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#MARKET} so check the Javadoc + * for that method for parameter details. * * @return the requested {@link Order} * @@ -239,10 +239,11 @@ public Order requestMarketOrder(String symbol, Integer quantity, OrderSide side, /** * A market order is a request to buy or sell a security at the currently available market price. It provides the - * most likely method of filling an order. Market orders fill nearly instantaneously. This method calls {@link - * #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, Double, - * Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#MARKET} and {@link - * OrderTimeInForce#GOOD_UNTIL_CANCELLED} with a fractional quantity so check the Javadoc for that method for parameter details. + * most likely method of filling an order. Market orders fill nearly instantaneously. This method calls + * {@link #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, + * Double, Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#MARKET} and + * {@link OrderTimeInForce#GOOD_UNTIL_CANCELLED} with a fractional quantity so check the Javadoc for that method for + * parameter details. * * @return the requested {@link Order} * @@ -250,17 +251,18 @@ public Order requestMarketOrder(String symbol, Integer quantity, OrderSide side, */ public Order requestFractionalMarketOrder(String symbol, Double quantity, OrderSide side) throws AlpacaClientException { - return requestOrder(symbol, quantity, null, side, OrderType.MARKET, OrderTimeInForce.GOOD_UNTIL_CANCELLED, null, null, null, + return requestOrder(symbol, quantity, null, side, OrderType.MARKET, OrderTimeInForce.GOOD_UNTIL_CANCELLED, null, + null, null, null, null, null, null, null, null, null); } /** * A market order is a request to buy or sell a security at the currently available market price. It provides the - * most likely method of filling an order. Market orders fill nearly instantaneously. This method calls {@link - * #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, Double, - * Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#MARKET} and {@link - * OrderTimeInForce#GOOD_UNTIL_CANCELLED} with a notional dollar amount so check the Javadoc for that method for the parameter - * details. + * most likely method of filling an order. Market orders fill nearly instantaneously. This method calls + * {@link #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, + * Double, Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#MARKET} and + * {@link OrderTimeInForce#GOOD_UNTIL_CANCELLED} with a notional dollar amount so check the Javadoc for that method + * for the parameter details. * * @return the requested {@link Order} * @@ -268,7 +270,8 @@ public Order requestFractionalMarketOrder(String symbol, Double quantity, OrderS */ public Order requestNotionalMarketOrder(String symbol, Double notional, OrderSide side) throws AlpacaClientException { - return requestOrder(symbol, null, notional, side, OrderType.MARKET, OrderTimeInForce.GOOD_UNTIL_CANCELLED, null, null, null, + return requestOrder(symbol, null, notional, side, OrderType.MARKET, OrderTimeInForce.GOOD_UNTIL_CANCELLED, null, + null, null, null, null, null, null, null, null, null); } @@ -276,10 +279,10 @@ public Order requestNotionalMarketOrder(String symbol, Double notional, OrderSid * A limit order is an order to buy or sell at a specified price or better. A buy limit order (a limit order to buy) * is executed at the specified limit price or lower (i.e., better). Conversely, a sell limit order (a limit order * to sell) is executed at the specified limit price or higher (better). Unlike a market order, you have to specify - * the limit price parameter when submitting your order. This method calls {@link #requestOrder(String, Double, - * Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, Double, Boolean, String, OrderClass, - * Double, Double, Double)} with {@link OrderType#LIMIT} so check the Javadoc for that method for the parameter - * details. + * the limit price parameter when submitting your order. This method calls + * {@link #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, + * Double, Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#LIMIT} so check the Javadoc + * for that method for the parameter details. * * @return the requested {@link Order} * @@ -296,10 +299,10 @@ public Order requestLimitOrder(String symbol, Double quantity, OrderSide side, O * ensuring a higher probability of achieving a predetermined entry or exit price. Once the market price crosses the * specified stop price, the stop order becomes a market order. Alpaca converts buy stop orders into stop limit * orders with a limit price that is 4% higher than a stop price < $50 (or 2.5% higher than a stop price >= - * $50). Sell stop orders are not converted into stop limit orders. This method calls {@link #requestOrder(String, - * Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, Double, Boolean, String, - * OrderClass, Double, Double, Double)} with {@link OrderType#STOP} so check the Javadoc for that method for - * parameter details. + * $50). Sell stop orders are not converted into stop limit orders. This method calls + * {@link #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, + * Double, Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#STOP} so check the Javadoc + * for that method for parameter details. * * @return the requested {@link Order} * @@ -315,10 +318,10 @@ public Order requestStopOrder(String symbol, Integer quantity, OrderSide side, O * A stop-limit order is a conditional trade over a set time frame that combines the features of a stop order with * those of a limit order and is used to mitigate risk. The stop-limit order will be executed at a specified limit * price, or better, after a given stop price has been reached. Once the stop price is reached, the stop-limit order - * becomes a limit order to buy or sell at the limit price or better. This method calls {@link #requestOrder(String, - * Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, Double, Boolean, String, - * OrderClass, Double, Double, Double)} with {@link OrderType#STOP_LIMIT} so check the Javadoc for that method for - * parameter details. + * becomes a limit order to buy or sell at the limit price or better. This method calls + * {@link #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, + * Double, Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#STOP_LIMIT} so check the + * Javadoc for that method for parameter details. * * @return the requested {@link Order} * @@ -332,10 +335,10 @@ public Order requestStopLimitOrder(String symbol, Integer quantity, OrderSide si /** * A bracket order is a chain of three orders that can be used to manage your position entry and exit. It is a - * common use case of an OTOCO (One Triggers OCO {One Cancels Other}) order. This method calls {@link - * #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, Double, - * Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#MARKET} and with parameters for a - * bracket order so check the Javadoc for that method for parameter details. + * common use case of an OTOCO (One Triggers OCO {One Cancels Other}) order. This method calls + * {@link #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, + * Double, Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#MARKET} and with parameters + * for a bracket order so check the Javadoc for that method for parameter details. * * @return the requested {@link Order} * @@ -351,10 +354,10 @@ public Order requestMarketBracketOrder(String symbol, Integer quantity, OrderSid /** * A bracket order is a chain of three orders that can be used to manage your position entry and exit. It is a - * common use case of an OTOCO (One Triggers OCO {One Cancels Other}) order. This method calls {@link - * #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, Double, - * Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#LIMIT} and with parameters for a - * bracket order so check the Javadoc for that method for parameter details. + * common use case of an OTOCO (One Triggers OCO {One Cancels Other}) order. This method calls + * {@link #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, + * Double, Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#LIMIT} and with parameters + * for a bracket order so check the Javadoc for that method for parameter details. * * @return the requested {@link Order} * @@ -372,10 +375,10 @@ public Order requestLimitBracketOrder(String symbol, Integer quantity, OrderSide * OCO (One-Cancels-Other) is another type of advanced order type. This is a set of two orders with the same side * (buy/buy or sell/sell) and currently only exit order is supported. In other words, this is the second part of the * bracket orders where the entry order is already filled, and you can submit the take-profit and stop-loss in one - * order submission. This method calls {@link #requestOrder(String, Double, Double, OrderSide, OrderType, - * OrderTimeInForce, Double, Double, Double, Double, Boolean, String, OrderClass, Double, Double, Double)} with - * parameters for a {@link OrderClass#ONE_CANCELS_OTHER} so check the Javadoc for that method for the parameter - * details. + * order submission. This method calls + * {@link #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, + * Double, Boolean, String, OrderClass, Double, Double, Double)} with parameters for a + * {@link OrderClass#ONE_CANCELS_OTHER} so check the Javadoc for that method for the parameter details. * * @return the requested {@link Order} * @@ -391,10 +394,10 @@ public Order requestOCOOrder(String symbol, Integer quantity, OrderSide side, /** * OTO (One-Triggers-Other) is a variant of bracket order. It takes one of the take-profit or stop-loss order in - * addition to the entry order. This method calls {@link #requestOrder(String, Double, Double, OrderSide, OrderType, - * OrderTimeInForce, Double, Double, Double, Double, Boolean, String, OrderClass, Double, Double, Double)} with - * {@link OrderType#LIMIT} and with parameters for a {@link OrderClass#ONE_CANCELS_OTHER} so check the Javadoc for - * that method for parameter details. + * addition to the entry order. This method calls + * {@link #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, + * Double, Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#LIMIT} and with parameters + * for a {@link OrderClass#ONE_CANCELS_OTHER} so check the Javadoc for that method for parameter details. * * @return the requested {@link Order} * @@ -410,10 +413,10 @@ public Order requestOTOMarketOrder(String symbol, Integer quantity, OrderSide si /** * OTO (One-Triggers-Other) is a variant of bracket order. It takes one of the take-profit or stop-loss order in - * addition to the entry order. This method calls {@link #requestOrder(String, Double, Double, OrderSide, OrderType, - * OrderTimeInForce, Double, Double, Double, Double, Boolean, String, OrderClass, Double, Double, Double)} with - * {@link OrderType#LIMIT} and with parameters for a {@link OrderClass#ONE_CANCELS_OTHER} so check the Javadoc for - * that method for parameter details. + * addition to the entry order. This method calls + * {@link #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, + * Double, Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#LIMIT} and with parameters + * for a {@link OrderClass#ONE_CANCELS_OTHER} so check the Javadoc for that method for parameter details. * * @return the requested {@link Order} * @@ -429,10 +432,10 @@ public Order requestOTOLimitOrder(String symbol, Integer quantity, OrderSide sid /** * OTO (One-Triggers-Other) is a variant of bracket order. It takes one of the take-profit or stop-loss order in - * addition to the entry order. This method calls {@link #requestOrder(String, Double, Double, OrderSide, OrderType, - * OrderTimeInForce, Double, Double, Double, Double, Boolean, String, OrderClass, Double, Double, Double)} with - * {@link OrderType#STOP} and with parameters for a {@link OrderClass#ONE_CANCELS_OTHER} so check the Javadoc for - * that method for parameter details. + * addition to the entry order. This method calls + * {@link #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, + * Double, Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#STOP} and with parameters for + * a {@link OrderClass#ONE_CANCELS_OTHER} so check the Javadoc for that method for parameter details. * * @return the requested {@link Order} * @@ -448,10 +451,11 @@ public Order requestOTOStopOrder(String symbol, Integer quantity, OrderSide side /** * OTO (One-Triggers-Other) is a variant of bracket order. It takes one of the take-profit or stop-loss order in - * addition to the entry order. This method calls {@link #requestOrder(String, Double, Double, OrderSide, OrderType, - * OrderTimeInForce, Double, Double, Double, Double, Boolean, String, OrderClass, Double, Double, Double)} with - * {@link OrderType#STOP_LIMIT} and with parameters for a {@link OrderClass#ONE_CANCELS_OTHER} so check the Javadoc - * for that method for parameter details. + * addition to the entry order. This method calls + * {@link #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, + * Double, Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#STOP_LIMIT} and with + * parameters for a {@link OrderClass#ONE_CANCELS_OTHER} so check the Javadoc for that method for parameter + * details. * * @return the requested {@link Order} * @@ -468,10 +472,10 @@ public Order requestOTOStopLimitOrder(String symbol, Integer quantity, OrderSide /** * Trailing stop orders allow you to continuously and automatically keep updating the stop price threshold based on - * the stock price movement. This method calls {@link #requestOrder(String, Double, Double, OrderSide, OrderType, - * OrderTimeInForce, Double, Double, Double, Double, Boolean, String, OrderClass, Double, Double, Double)} with - * {@link OrderType#TRAILING_STOP} and with parameters for a {@link OrderType#TRAILING_STOP} so check the Javadoc - * for that method for parameter details. + * the stock price movement. This method calls + * {@link #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, + * Double, Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#TRAILING_STOP} and with + * parameters for a {@link OrderType#TRAILING_STOP} so check the Javadoc for that method for parameter details. * * @return the requested {@link Order} * @@ -485,10 +489,10 @@ public Order requestTrailingStopPriceOrder(String symbol, Integer quantity, Orde /** * Trailing stop orders allow you to continuously and automatically keep updating the stop price threshold based on - * the stock price movement. This method calls {@link #requestOrder(String, Double, Double, OrderSide, OrderType, - * OrderTimeInForce, Double, Double, Double, Double, Boolean, String, OrderClass, Double, Double, Double)} with - * {@link OrderType#TRAILING_STOP} and with parameters for a {@link OrderType#TRAILING_STOP} so check the Javadoc - * for that method for parameter details. + * the stock price movement. This method calls + * {@link #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, + * Double, Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#TRAILING_STOP} and with + * parameters for a {@link OrderType#TRAILING_STOP} so check the Javadoc for that method for parameter details. * * @return the requested {@link Order} * @@ -625,8 +629,8 @@ public List cancelAll() throws AlpacaClientException { } /** - * Attempts to cancel an open {@link Order}. If the {@link Order} is no longer cancelable (example: {@link - * Order#getStatus()} == {@link OrderStatus#FILLED}), the server will respond with status + * Attempts to cancel an open {@link Order}. If the {@link Order} is no longer cancelable (example: + * {@link Order#getStatus()} == {@link OrderStatus#FILLED}), the server will respond with status * 422, and reject the request. * * @param orderID the {@link Order#getId()} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java index 9241f066..9c924c11 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java @@ -35,8 +35,8 @@ import static net.jacobpeterson.alpaca.util.gson.GsonUtil.GSON; /** - * {@link MarketDataWebsocket} is an {@link AlpacaWebsocket} implementation and provides the {@link - * MarketDataWebsocketInterface} interface for + * {@link MarketDataWebsocket} is an {@link AlpacaWebsocket} implementation and provides the + * {@link MarketDataWebsocketInterface} interface for * Realtime Market Data for both crypto * and stocks. */ @@ -243,8 +243,8 @@ private boolean isSuccessMessageAuthenticated(SuccessMessage successMessage) { } /** - * Handles a {@link SubscriptionsMessage} for updating {@link #listenedMarketDataMessageTypes} and returns a {@link - * List} of currently subscribe symbols or null. + * Handles a {@link SubscriptionsMessage} for updating {@link #listenedMarketDataMessageTypes} and returns a + * {@link List} of currently subscribe symbols or null. * * @param marketDataMessageType the {@link MarketDataMessageType} * @param newSubscribedSymbols the new subscribed symbols from {@link SubscriptionsMessage} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocket.java index cfbf2106..92a15c0c 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocket.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocket.java @@ -35,8 +35,8 @@ import static net.jacobpeterson.alpaca.util.gson.GsonUtil.GSON; /** - * {@link StreamingWebsocket} is an {@link AlpacaWebsocket} implementation and provides the {@link - * StreamingWebsocketInterface} interface for + * {@link StreamingWebsocket} is an {@link AlpacaWebsocket} implementation and provides the + * {@link StreamingWebsocketInterface} interface for * Streaming */ public class StreamingWebsocket extends AlpacaWebsocket diff --git a/src/main/resources/alpaca.default.properties b/src/main/resources/alpaca.default.properties index 0cef7e43..7da46f1c 100644 --- a/src/main/resources/alpaca.default.properties +++ b/src/main/resources/alpaca.default.properties @@ -4,7 +4,6 @@ #endpoint_api_type = #data_api_type = #user_agent = - #Defaults: endpoint_api_type=paper data_api_type=iex diff --git a/src/test/java/net/jacobpeterson/alpaca/test/live/AlpacaAPITest.java b/src/test/java/net/jacobpeterson/alpaca/test/live/AlpacaAPITest.java index f9760235..9e6d6136 100644 --- a/src/test/java/net/jacobpeterson/alpaca/test/live/AlpacaAPITest.java +++ b/src/test/java/net/jacobpeterson/alpaca/test/live/AlpacaAPITest.java @@ -15,7 +15,6 @@ import net.jacobpeterson.alpaca.model.endpoint.orders.enums.CurrentOrderStatus; import net.jacobpeterson.alpaca.rest.AlpacaClientException; import net.jacobpeterson.alpaca.rest.endpoint.account.AccountEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.accountactivities.AccountActivitiesEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.accountconfiguration.AccountConfigurationEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.clock.ClockEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.orders.OrdersEndpoint; @@ -156,8 +155,9 @@ public void testAccountEndpointGet() throws AlpacaClientException, NumberFormatE } /** - * Tests @{@link AccountActivitiesEndpoint#get(ZonedDateTime, ZonedDateTime, ZonedDateTime, SortDirection, Integer, - * String, ActivityType...)} one {@link AccountActivity} exists until now. + * Tests + * @{@link AccountActivitiesEndpoint#get(ZonedDateTime, ZonedDateTime, ZonedDateTime, SortDirection, Integer, + *String, ActivityType...)} one {@link AccountActivity} exists until now. * * @throws AlpacaClientException thrown for {@link AlpacaClientException}s */ @@ -246,7 +246,8 @@ public void testAccountConfigurationEndpointSet() throws AlpacaClientException { } /** - * Test {@link OrdersEndpoint#get(CurrentOrderStatus, Integer, ZonedDateTime, ZonedDateTime, SortDirection, Boolean, + * Test + * {@link OrdersEndpoint#get(CurrentOrderStatus, Integer, ZonedDateTime, ZonedDateTime, SortDirection, Boolean, * Collection)} one {@link Order} exists until now. * * @throws AlpacaClientException thrown for {@link AlpacaClientException}s From cf282160700a5edf396a3ac0de0d32a7e8d88e19 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Sun, 20 Aug 2023 16:36:10 -0600 Subject: [PATCH 49/84] Update `.editorconfig` --- .editorconfig | 1067 +++++++++++++++++++++++++++++++++++-------------- README.md | 1 - 2 files changed, 776 insertions(+), 292 deletions(-) diff --git a/.editorconfig b/.editorconfig index 604f1553..818a4c9e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,8 +11,33 @@ ij_formatter_off_tag = @formatter:off ij_formatter_on_tag = @formatter:on ij_formatter_tags_enabled = true ij_smart_tabs = false +ij_visual_guides = ij_wrap_on_typing = false +[*.css] +ij_css_align_closing_brace_with_properties = false +ij_css_blank_lines_around_nested_selector = 1 +ij_css_blank_lines_between_blocks = 1 +ij_css_block_comment_add_space = false +ij_css_brace_placement = end_of_line +ij_css_enforce_quotes_on_format = false +ij_css_hex_color_long_format = false +ij_css_hex_color_lower_case = false +ij_css_hex_color_short_format = false +ij_css_hex_color_upper_case = false +ij_css_keep_blank_lines_in_code = 2 +ij_css_keep_indents_on_empty_lines = false +ij_css_keep_single_line_blocks = false +ij_css_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_css_space_after_colon = true +ij_css_space_before_opening_brace = true +ij_css_use_double_quotes = true +ij_css_value_alignment = do_not_align + +[*.feature] +indent_size = 2 +ij_gherkin_keep_indents_on_empty_lines = false + [*.java] ij_java_align_consecutive_assignments = false ij_java_align_consecutive_variable_declarations = false @@ -22,17 +47,21 @@ ij_java_align_multiline_array_initializer_expression = false ij_java_align_multiline_assignment = false ij_java_align_multiline_binary_operation = false ij_java_align_multiline_chained_methods = false +ij_java_align_multiline_deconstruction_list_components = false ij_java_align_multiline_extends_list = false ij_java_align_multiline_for = false ij_java_align_multiline_method_parentheses = false ij_java_align_multiline_parameters = false ij_java_align_multiline_parameters_in_calls = false ij_java_align_multiline_parenthesized_expression = false +ij_java_align_multiline_records = false ij_java_align_multiline_resources = false ij_java_align_multiline_ternary_operation = false +ij_java_align_multiline_text_blocks = false ij_java_align_multiline_throws_list = false ij_java_align_subsequent_simple_methods = false ij_java_align_throws_keyword = false +ij_java_align_types_in_multi_catch = false ij_java_annotation_parameter_wrap = normal ij_java_array_initializer_new_line_after_left_brace = false ij_java_array_initializer_right_brace_on_new_line = false @@ -57,18 +86,22 @@ ij_java_blank_lines_before_imports = 1 ij_java_blank_lines_before_method_body = 0 ij_java_blank_lines_before_package = 0 ij_java_block_brace_style = end_of_line +ij_java_block_comment_add_space = false ij_java_block_comment_at_first_column = true +ij_java_builder_methods = ij_java_call_parameters_new_line_after_left_paren = false ij_java_call_parameters_right_paren_on_new_line = false ij_java_call_parameters_wrap = normal ij_java_case_statement_on_separate_line = true ij_java_catch_on_new_line = false -ij_java_class_annotation_wrap = split_into_lines +ij_java_class_annotation_wrap = normal ij_java_class_brace_style = end_of_line -ij_java_class_count_to_use_import_on_demand = 20 -ij_java_class_names_in_javadoc = 1 +ij_java_class_count_to_use_import_on_demand = 999 +ij_java_class_names_in_javadoc = 3 +ij_java_deconstruction_list_wrap = normal ij_java_do_not_indent_top_level_class_members = false ij_java_do_not_wrap_after_single_annotation = false +ij_java_do_not_wrap_after_single_annotation_in_parameter = false ij_java_do_while_brace_force = always ij_java_doc_add_blank_line_after_description = true ij_java_doc_add_blank_line_after_param_comments = true @@ -76,7 +109,7 @@ ij_java_doc_add_blank_line_after_return = true ij_java_doc_add_p_tag_on_empty_lines = true ij_java_doc_align_exception_comments = true ij_java_doc_align_param_comments = true -ij_java_doc_do_not_wrap_if_one_line = true +ij_java_doc_do_not_wrap_if_one_line = false ij_java_doc_enable_formatting = true ij_java_doc_enable_leading_asterisks = true ij_java_doc_indent_on_continuation = false @@ -89,10 +122,31 @@ ij_java_doc_param_description_on_new_line = false ij_java_doc_preserve_line_breaks = false ij_java_doc_use_throws_not_exception_tag = true ij_java_else_on_new_line = false -ij_java_enum_constants_wrap = normal +ij_java_entity_dd_prefix = +ij_java_entity_dd_suffix = EJB +ij_java_entity_eb_prefix = +ij_java_entity_eb_suffix = Bean +ij_java_entity_hi_prefix = +ij_java_entity_hi_suffix = Home +ij_java_entity_lhi_prefix = Local +ij_java_entity_lhi_suffix = Home +ij_java_entity_li_prefix = Local +ij_java_entity_li_suffix = +ij_java_entity_pk_class = java.lang.String +ij_java_entity_ri_prefix = +ij_java_entity_ri_suffix = +ij_java_entity_vo_prefix = +ij_java_entity_vo_suffix = VO +ij_java_enum_constants_wrap = split_into_lines ij_java_extends_keyword_wrap = normal ij_java_extends_list_wrap = normal -ij_java_field_annotation_wrap = split_into_lines +ij_java_field_annotation_wrap = normal +ij_java_field_name_prefix = +ij_java_field_name_suffix = +ij_java_filter_class_prefix = +ij_java_filter_class_suffix = +ij_java_filter_dd_prefix = +ij_java_filter_dd_suffix = ij_java_finally_on_new_line = false ij_java_for_brace_force = always ij_java_for_statement_new_line_after_left_paren = false @@ -109,6 +163,7 @@ ij_java_keep_blank_lines_before_right_brace = 1 ij_java_keep_blank_lines_between_package_declaration_and_header = 1 ij_java_keep_blank_lines_in_code = 1 ij_java_keep_blank_lines_in_declarations = 1 +ij_java_keep_builder_methods_indents = false ij_java_keep_control_statement_in_one_line = true ij_java_keep_first_column_comment = true ij_java_keep_indents_on_empty_lines = false @@ -118,24 +173,44 @@ ij_java_keep_simple_blocks_in_one_line = true ij_java_keep_simple_classes_in_one_line = true ij_java_keep_simple_lambdas_in_one_line = true ij_java_keep_simple_methods_in_one_line = true +ij_java_label_indent_absolute = false +ij_java_label_indent_size = 0 ij_java_lambda_brace_style = end_of_line ij_java_layout_static_imports_separately = true -ij_java_line_comment_add_space = true -ij_java_line_comment_at_first_column = false -ij_java_method_annotation_wrap = split_into_lines +ij_java_line_comment_add_space = false +ij_java_line_comment_add_space_on_reformat = false +ij_java_line_comment_at_first_column = true +ij_java_listener_class_prefix = +ij_java_listener_class_suffix = +ij_java_local_variable_name_prefix = +ij_java_local_variable_name_suffix = +ij_java_message_dd_prefix = +ij_java_message_dd_suffix = EJB +ij_java_message_eb_prefix = +ij_java_message_eb_suffix = Bean +ij_java_method_annotation_wrap = normal ij_java_method_brace_style = end_of_line ij_java_method_call_chain_wrap = normal ij_java_method_parameters_new_line_after_left_paren = false ij_java_method_parameters_right_paren_on_new_line = false ij_java_method_parameters_wrap = normal ij_java_modifier_list_wrap = false -ij_java_names_count_to_use_import_on_demand = 20 +ij_java_multi_catch_types_wrap = normal +ij_java_names_count_to_use_import_on_demand = 999 +ij_java_new_line_after_lparen_in_annotation = false +ij_java_new_line_after_lparen_in_deconstruction_pattern = false +ij_java_new_line_after_lparen_in_record_header = false +ij_java_packages_to_use_import_on_demand = ij_java_parameter_annotation_wrap = normal +ij_java_parameter_name_prefix = +ij_java_parameter_name_suffix = ij_java_parentheses_expression_new_line_after_left_paren = false ij_java_parentheses_expression_right_paren_on_new_line = false ij_java_place_assignment_sign_on_next_line = false ij_java_prefer_longer_names = true ij_java_prefer_parameters_wrap = false +ij_java_record_components_wrap = normal +ij_java_repeat_annotations = ij_java_repeat_synchronized = true ij_java_replace_instanceof_and_cast = false ij_java_replace_null_check = true @@ -143,6 +218,27 @@ ij_java_replace_sum_lambda_with_method_ref = true ij_java_resource_list_new_line_after_left_paren = false ij_java_resource_list_right_paren_on_new_line = false ij_java_resource_list_wrap = normal +ij_java_rparen_on_new_line_in_annotation = false +ij_java_rparen_on_new_line_in_deconstruction_pattern = false +ij_java_rparen_on_new_line_in_record_header = false +ij_java_servlet_class_prefix = +ij_java_servlet_class_suffix = +ij_java_servlet_dd_prefix = +ij_java_servlet_dd_suffix = +ij_java_session_dd_prefix = +ij_java_session_dd_suffix = EJB +ij_java_session_eb_prefix = +ij_java_session_eb_suffix = Bean +ij_java_session_hi_prefix = +ij_java_session_hi_suffix = Home +ij_java_session_lhi_prefix = Local +ij_java_session_lhi_suffix = Home +ij_java_session_li_prefix = Local +ij_java_session_li_suffix = +ij_java_session_ri_prefix = +ij_java_session_ri_suffix = +ij_java_session_si_prefix = +ij_java_session_si_suffix = Service ij_java_space_after_closing_angle_bracket_in_type_argument = false ij_java_space_after_colon = true ij_java_space_after_comma = true @@ -160,6 +256,7 @@ ij_java_space_before_class_left_brace = true ij_java_space_before_colon = true ij_java_space_before_colon_in_foreach = true ij_java_space_before_comma = false +ij_java_space_before_deconstruction_list = false ij_java_space_before_do_left_brace = true ij_java_space_before_else_keyword = true ij_java_space_before_else_left_brace = true @@ -190,6 +287,7 @@ ij_java_space_within_empty_array_initializer_braces = false ij_java_space_within_empty_method_call_parentheses = false ij_java_space_within_empty_method_parentheses = false ij_java_spaces_around_additive_operators = true +ij_java_spaces_around_annotation_eq = true ij_java_spaces_around_assignment_operators = true ij_java_spaces_around_bitwise_operators = true ij_java_spaces_around_equality_operators = true @@ -208,24 +306,31 @@ ij_java_spaces_within_braces = false ij_java_spaces_within_brackets = false ij_java_spaces_within_cast_parentheses = false ij_java_spaces_within_catch_parentheses = false +ij_java_spaces_within_deconstruction_list = false ij_java_spaces_within_for_parentheses = false ij_java_spaces_within_if_parentheses = false ij_java_spaces_within_method_call_parentheses = false ij_java_spaces_within_method_parentheses = false ij_java_spaces_within_parentheses = false +ij_java_spaces_within_record_header = false ij_java_spaces_within_switch_parentheses = false ij_java_spaces_within_synchronized_parentheses = false ij_java_spaces_within_try_parentheses = false ij_java_spaces_within_while_parentheses = false ij_java_special_else_if_treatment = true +ij_java_static_field_name_prefix = +ij_java_static_field_name_suffix = +ij_java_subclass_name_prefix = ij_java_subclass_name_suffix = Impl ij_java_ternary_operation_signs_on_next_line = false ij_java_ternary_operation_wrap = normal +ij_java_test_name_prefix = ij_java_test_name_suffix = Test ij_java_throws_keyword_wrap = normal ij_java_throws_list_wrap = normal ij_java_use_external_annotations = false ij_java_use_fq_class_names = false +ij_java_use_relative_indents = false ij_java_use_single_class_imports = true ij_java_variable_annotation_wrap = normal ij_java_visibility = public @@ -233,209 +338,99 @@ ij_java_while_brace_force = always ij_java_while_on_new_line = false ij_java_wrap_comments = true ij_java_wrap_first_method_in_call_chain = false -ij_java_wrap_long_lines = true +ij_java_wrap_long_lines = false -[*.json] +[*.less] indent_size = 2 -ij_json_keep_blank_lines_in_code = 0 -ij_json_keep_indents_on_empty_lines = false -ij_json_keep_line_breaks = true -ij_json_space_after_colon = true -ij_json_space_after_comma = true -ij_json_space_before_colon = true -ij_json_space_before_comma = false -ij_json_spaces_within_braces = false -ij_json_spaces_within_brackets = false -ij_json_wrap_long_lines = false +ij_less_align_closing_brace_with_properties = false +ij_less_blank_lines_around_nested_selector = 1 +ij_less_blank_lines_between_blocks = 1 +ij_less_block_comment_add_space = false +ij_less_brace_placement = 0 +ij_less_enforce_quotes_on_format = false +ij_less_hex_color_long_format = false +ij_less_hex_color_lower_case = false +ij_less_hex_color_short_format = false +ij_less_hex_color_upper_case = false +ij_less_keep_blank_lines_in_code = 2 +ij_less_keep_indents_on_empty_lines = false +ij_less_keep_single_line_blocks = false +ij_less_line_comment_add_space = false +ij_less_line_comment_at_first_column = false +ij_less_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_less_space_after_colon = true +ij_less_space_before_opening_brace = true +ij_less_use_double_quotes = true +ij_less_value_alignment = 0 -[*.properties] -ij_properties_align_group_field_declarations = false +[*.proto] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 4 +ij_protobuf_keep_blank_lines_in_code = 2 +ij_protobuf_keep_indents_on_empty_lines = false +ij_protobuf_keep_line_breaks = true +ij_protobuf_space_after_comma = true +ij_protobuf_space_before_comma = false +ij_protobuf_spaces_around_assignment_operators = true +ij_protobuf_spaces_within_braces = false +ij_protobuf_spaces_within_brackets = false -[*.scala] +[*.sass] +indent_size = 2 +ij_sass_align_closing_brace_with_properties = false +ij_sass_blank_lines_around_nested_selector = 1 +ij_sass_blank_lines_between_blocks = 1 +ij_sass_brace_placement = 0 +ij_sass_enforce_quotes_on_format = false +ij_sass_hex_color_long_format = false +ij_sass_hex_color_lower_case = false +ij_sass_hex_color_short_format = false +ij_sass_hex_color_upper_case = false +ij_sass_keep_blank_lines_in_code = 2 +ij_sass_keep_indents_on_empty_lines = false +ij_sass_keep_single_line_blocks = false +ij_sass_line_comment_add_space = false +ij_sass_line_comment_at_first_column = false +ij_sass_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_sass_space_after_colon = true +ij_sass_space_before_opening_brace = true +ij_sass_use_double_quotes = true +ij_sass_value_alignment = 0 + +[*.scss] +indent_size = 2 +ij_scss_align_closing_brace_with_properties = false +ij_scss_blank_lines_around_nested_selector = 1 +ij_scss_blank_lines_between_blocks = 1 +ij_scss_block_comment_add_space = false +ij_scss_brace_placement = 0 +ij_scss_enforce_quotes_on_format = false +ij_scss_hex_color_long_format = false +ij_scss_hex_color_lower_case = false +ij_scss_hex_color_short_format = false +ij_scss_hex_color_upper_case = false +ij_scss_keep_blank_lines_in_code = 2 +ij_scss_keep_indents_on_empty_lines = false +ij_scss_keep_single_line_blocks = false +ij_scss_line_comment_add_space = false +ij_scss_line_comment_at_first_column = false +ij_scss_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_scss_space_after_colon = true +ij_scss_space_before_opening_brace = true +ij_scss_use_double_quotes = true +ij_scss_value_alignment = 0 + +[*.vue] indent_size = 2 tab_width = 2 -ij_continuation_indent_size = 2 -ij_scala_align_composite_pattern = true -ij_scala_align_extends_with = 0 -ij_scala_align_group_field_declarations = false -ij_scala_align_if_else = false -ij_scala_align_in_columns_case_branch = false -ij_scala_align_multiline_binary_operation = false -ij_scala_align_multiline_chained_methods = false -ij_scala_align_multiline_for = true -ij_scala_align_multiline_parameters = true -ij_scala_align_multiline_parameters_in_calls = false -ij_scala_align_multiline_parenthesized_expression = false -ij_scala_align_tuple_elements = false -ij_scala_align_types_in_multiline_declarations = false -ij_scala_all_other_imports = all other imports -ij_scala_alternate_continuation_indent_for_params = 4 -ij_scala_binary_operation_wrap = off -ij_scala_blank_line = _______ blank line _______ -ij_scala_blank_lines_after_anonymous_class_header = 0 -ij_scala_blank_lines_after_class_header = 0 -ij_scala_blank_lines_after_imports = 1 -ij_scala_blank_lines_after_package = 1 -ij_scala_blank_lines_around_class = 1 -ij_scala_blank_lines_around_field = 0 -ij_scala_blank_lines_around_field_in_inner_scopes = 0 -ij_scala_blank_lines_around_field_in_interface = 0 -ij_scala_blank_lines_around_method = 1 -ij_scala_blank_lines_around_method_in_inner_scopes = 1 -ij_scala_blank_lines_around_method_in_interface = 1 -ij_scala_blank_lines_before_imports = 1 -ij_scala_blank_lines_before_method_body = 0 -ij_scala_blank_lines_before_package = 0 -ij_scala_block_brace_style = end_of_line -ij_scala_block_comment_at_first_column = true -ij_scala_call_parameters_new_line_after_lparen = 0 -ij_scala_call_parameters_right_paren_on_new_line = false -ij_scala_call_parameters_wrap = off -ij_scala_case_clause_brace_force = never -ij_scala_catch_on_new_line = false -ij_scala_class_annotation_wrap = split_into_lines -ij_scala_class_brace_style = end_of_line -ij_scala_closure_brace_force = never -ij_scala_do_not_align_block_expr_params = true -ij_scala_do_not_indent_case_clause_body = false -ij_scala_do_not_indent_tuples_close_brace = true -ij_scala_do_while_brace_force = never -ij_scala_else_on_new_line = false -ij_scala_enable_scaladoc_formatting = true -ij_scala_enforce_functional_syntax_for_unit = true -ij_scala_exclude_prefix = exclude: -ij_scala_extends_keyword_wrap = off -ij_scala_extends_list_wrap = off -ij_scala_field_annotation_wrap = split_into_lines -ij_scala_finally_brace_force = never -ij_scala_finally_on_new_line = false -ij_scala_for_brace_force = never -ij_scala_for_statement_wrap = off -ij_scala_formatter = 0 -ij_scala_if_brace_force = never -ij_scala_indent_braced_function_args = true -ij_scala_indent_case_from_switch = true -ij_scala_indent_first_parameter = true -ij_scala_indent_first_parameter_clause = false -ij_scala_indent_type_arguments = true -ij_scala_indent_type_parameters = true -ij_scala_insert_whitespaces_in_simple_one_line_method = true -ij_scala_keep_blank_lines_before_right_brace = 2 -ij_scala_keep_blank_lines_in_code = 2 -ij_scala_keep_blank_lines_in_declarations = 2 -ij_scala_keep_comments_on_same_line = true -ij_scala_keep_first_column_comment = false -ij_scala_keep_indents_on_empty_lines = false -ij_scala_keep_line_breaks = true -ij_scala_keep_one_line_lambdas_in_arg_list = false -ij_scala_keep_simple_blocks_in_one_line = false -ij_scala_keep_simple_methods_in_one_line = false -ij_scala_keep_xml_formatting = false -ij_scala_line_comment_at_first_column = true -ij_scala_method_annotation_wrap = split_into_lines -ij_scala_method_brace_force = never -ij_scala_method_brace_style = end_of_line -ij_scala_method_call_chain_wrap = off -ij_scala_method_parameters_new_line_after_left_paren = false -ij_scala_method_parameters_right_paren_on_new_line = false -ij_scala_method_parameters_wrap = off -ij_scala_modifier_list_wrap = false -ij_scala_multiline_string_align_dangling_closing_quotes = false -ij_scala_multiline_string_closing_quotes_on_new_line = true -ij_scala_multiline_string_insert_margin_on_enter = true -ij_scala_multiline_string_margin_char = | -ij_scala_multiline_string_margin_indent = 2 -ij_scala_multiline_string_opening_quotes_on_new_line = true -ij_scala_multiline_string_process_margin_on_copy_paste = true -ij_scala_newline_after_annotations = false -ij_scala_not_continuation_indent_for_params = false -ij_scala_parameter_annotation_wrap = off -ij_scala_parentheses_expression_new_line_after_left_paren = false -ij_scala_parentheses_expression_right_paren_on_new_line = false -ij_scala_place_closure_parameters_on_new_line = false -ij_scala_place_self_type_on_new_line = true -ij_scala_prefer_parameters_wrap = false -ij_scala_preserve_space_after_method_declaration_name = false -ij_scala_reformat_on_compile = false -ij_scala_replace_case_arrow_with_unicode_char = false -ij_scala_replace_for_generator_arrow_with_unicode_char = false -ij_scala_replace_lambda_with_greek_letter = false -ij_scala_replace_map_arrow_with_unicode_char = false -ij_scala_scalafmt_reformat_on_files_save = false -ij_scala_scalafmt_show_invalid_code_warnings = true -ij_scala_scalafmt_use_intellij_formatter_for_range_format = true -ij_scala_sd_align_exception_comments = true -ij_scala_sd_align_other_tags_comments = true -ij_scala_sd_align_parameters_comments = true -ij_scala_sd_align_return_comments = true -ij_scala_sd_blank_line_after_parameters_comments = false -ij_scala_sd_blank_line_after_return_comments = false -ij_scala_sd_blank_line_before_parameters = false -ij_scala_sd_blank_line_before_tags = true -ij_scala_sd_blank_line_between_parameters = false -ij_scala_sd_keep_blank_lines_between_tags = false -ij_scala_sd_preserve_spaces_in_tags = false -ij_scala_space_after_comma = true -ij_scala_space_after_for_semicolon = true -ij_scala_space_after_modifiers_constructor = false -ij_scala_space_after_type_colon = true -ij_scala_space_before_brace_method_call = true -ij_scala_space_before_class_left_brace = true -ij_scala_space_before_infix_like_method_parentheses = false -ij_scala_space_before_infix_method_call_parentheses = false -ij_scala_space_before_infix_operator_like_method_call_parentheses = true -ij_scala_space_before_method_call_parentheses = false -ij_scala_space_before_method_left_brace = true -ij_scala_space_before_method_parentheses = false -ij_scala_space_before_type_colon = false -ij_scala_space_before_type_parameter_in_def_list = false -ij_scala_space_before_type_parameter_list = false -ij_scala_space_inside_closure_braces = true -ij_scala_space_inside_self_type_braces = true -ij_scala_space_within_empty_method_call_parentheses = false -ij_scala_spaces_around_at_in_patterns = false -ij_scala_spaces_in_imports = false -ij_scala_spaces_in_one_line_blocks = false -ij_scala_spaces_within_brackets = false -ij_scala_spaces_within_for_parentheses = false -ij_scala_spaces_within_if_parentheses = false -ij_scala_spaces_within_method_call_parentheses = false -ij_scala_spaces_within_method_parentheses = false -ij_scala_spaces_within_parentheses = false -ij_scala_spaces_within_while_parentheses = false -ij_scala_special_else_if_treatment = true -ij_scala_trailing_comma_arg_list_enabled = true -ij_scala_trailing_comma_import_selector_enabled = false -ij_scala_trailing_comma_mode = trailing_comma_keep -ij_scala_trailing_comma_params_enabled = true -ij_scala_trailing_comma_pattern_arg_list_enabled = false -ij_scala_trailing_comma_tuple_enabled = false -ij_scala_trailing_comma_tuple_type_enabled = false -ij_scala_trailing_comma_type_params_enabled = false -ij_scala_try_brace_force = never -ij_scala_type_annotation_exclude_constant = true -ij_scala_type_annotation_exclude_in_dialect_sources = true -ij_scala_type_annotation_exclude_in_test_sources = false -ij_scala_type_annotation_exclude_member_of_anonymous_class = false -ij_scala_type_annotation_exclude_member_of_private_class = false -ij_scala_type_annotation_exclude_when_type_is_stable = true -ij_scala_type_annotation_function_parameter = false -ij_scala_type_annotation_implicit_modifier = true -ij_scala_type_annotation_local_definition = false -ij_scala_type_annotation_private_member = false -ij_scala_type_annotation_protected_member = true -ij_scala_type_annotation_public_member = true -ij_scala_type_annotation_structural_type = true -ij_scala_type_annotation_underscore_parameter = false -ij_scala_type_annotation_unit_type = true -ij_scala_use_alternate_continuation_indent_for_params = false -ij_scala_use_scaladoc2_formatting = false -ij_scala_variable_annotation_wrap = off -ij_scala_while_brace_force = never -ij_scala_while_on_new_line = false -ij_scala_wrap_before_with_keyword = false -ij_scala_wrap_first_method_in_call_chain = false -ij_scala_wrap_long_lines = false +ij_continuation_indent_size = 4 +ij_vue_indent_children_of_top_level = template +ij_vue_interpolation_new_line_after_start_delimiter = true +ij_vue_interpolation_new_line_before_end_delimiter = true +ij_vue_interpolation_wrap = off +ij_vue_keep_indents_on_empty_lines = false +ij_vue_spaces_within_interpolation_expressions = true [.editorconfig] ij_editorconfig_align_group_field_declarations = false @@ -445,27 +440,402 @@ ij_editorconfig_space_before_colon = false ij_editorconfig_space_before_comma = false ij_editorconfig_spaces_around_assignment_operators = true -[{*.gant,*.groovy,*.gradle,*.gdsl,*.gy}] +[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.pom,*.rng,*.tld,*.wadl,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] +ij_xml_align_attributes = true +ij_xml_align_text = false +ij_xml_attribute_wrap = normal +ij_xml_block_comment_add_space = false +ij_xml_block_comment_at_first_column = true +ij_xml_keep_blank_lines = 2 +ij_xml_keep_indents_on_empty_lines = false +ij_xml_keep_line_breaks = true +ij_xml_keep_line_breaks_in_text = true +ij_xml_keep_whitespaces = false +ij_xml_keep_whitespaces_around_cdata = preserve +ij_xml_keep_whitespaces_inside_cdata = false +ij_xml_line_comment_at_first_column = true +ij_xml_space_after_tag_name = false +ij_xml_space_around_equals_in_attribute = false +ij_xml_space_inside_empty_tag = false +ij_xml_text_wrap = normal +ij_xml_use_custom_settings = false + +[{*.ats,*.cts,*.mts,*.ts}] +ij_typescript_align_imports = false +ij_typescript_align_multiline_array_initializer_expression = false +ij_typescript_align_multiline_binary_operation = false +ij_typescript_align_multiline_chained_methods = false +ij_typescript_align_multiline_extends_list = false +ij_typescript_align_multiline_for = false +ij_typescript_align_multiline_parameters = false +ij_typescript_align_multiline_parameters_in_calls = false +ij_typescript_align_multiline_ternary_operation = false +ij_typescript_align_object_properties = 0 +ij_typescript_align_union_types = false +ij_typescript_align_var_statements = 0 +ij_typescript_array_initializer_new_line_after_left_brace = false +ij_typescript_array_initializer_right_brace_on_new_line = false +ij_typescript_array_initializer_wrap = normal +ij_typescript_assignment_wrap = normal +ij_typescript_binary_operation_sign_on_next_line = false +ij_typescript_binary_operation_wrap = normal +ij_typescript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** +ij_typescript_blank_lines_after_imports = 1 +ij_typescript_blank_lines_around_class = 1 +ij_typescript_blank_lines_around_field = 0 +ij_typescript_blank_lines_around_field_in_interface = 0 +ij_typescript_blank_lines_around_function = 1 +ij_typescript_blank_lines_around_method = 1 +ij_typescript_blank_lines_around_method_in_interface = 1 +ij_typescript_block_brace_style = end_of_line +ij_typescript_block_comment_add_space = false +ij_typescript_block_comment_at_first_column = true +ij_typescript_call_parameters_new_line_after_left_paren = false +ij_typescript_call_parameters_right_paren_on_new_line = false +ij_typescript_call_parameters_wrap = normal +ij_typescript_catch_on_new_line = false +ij_typescript_chained_call_dot_on_new_line = true +ij_typescript_class_brace_style = end_of_line +ij_typescript_comma_on_new_line = false +ij_typescript_do_while_brace_force = always +ij_typescript_else_on_new_line = false +ij_typescript_enforce_trailing_comma = remove +ij_typescript_enum_constants_wrap = split_into_lines +ij_typescript_extends_keyword_wrap = normal +ij_typescript_extends_list_wrap = normal +ij_typescript_field_prefix = _ +ij_typescript_file_name_style = relaxed +ij_typescript_finally_on_new_line = false +ij_typescript_for_brace_force = always +ij_typescript_for_statement_new_line_after_left_paren = false +ij_typescript_for_statement_right_paren_on_new_line = false +ij_typescript_for_statement_wrap = split_into_lines +ij_typescript_force_quote_style = true +ij_typescript_force_semicolon_style = true +ij_typescript_function_expression_brace_style = end_of_line +ij_typescript_if_brace_force = always +ij_typescript_import_merge_members = global +ij_typescript_import_prefer_absolute_path = global +ij_typescript_import_sort_members = true +ij_typescript_import_sort_module_name = false +ij_typescript_import_use_node_resolution = true +ij_typescript_imports_wrap = normal +ij_typescript_indent_case_from_switch = true +ij_typescript_indent_chained_calls = true +ij_typescript_indent_package_children = 0 +ij_typescript_jsdoc_include_types = false +ij_typescript_jsx_attribute_value = braces +ij_typescript_keep_blank_lines_in_code = 1 +ij_typescript_keep_first_column_comment = true +ij_typescript_keep_indents_on_empty_lines = false +ij_typescript_keep_line_breaks = true +ij_typescript_keep_simple_blocks_in_one_line = true +ij_typescript_keep_simple_methods_in_one_line = true +ij_typescript_line_comment_add_space = true +ij_typescript_line_comment_at_first_column = false +ij_typescript_method_brace_style = end_of_line +ij_typescript_method_call_chain_wrap = normal +ij_typescript_method_parameters_new_line_after_left_paren = false +ij_typescript_method_parameters_right_paren_on_new_line = false +ij_typescript_method_parameters_wrap = normal +ij_typescript_object_literal_wrap = on_every_item +ij_typescript_object_types_wrap = on_every_item +ij_typescript_parentheses_expression_new_line_after_left_paren = false +ij_typescript_parentheses_expression_right_paren_on_new_line = false +ij_typescript_place_assignment_sign_on_next_line = false +ij_typescript_prefer_as_type_cast = false +ij_typescript_prefer_explicit_types_function_expression_returns = false +ij_typescript_prefer_explicit_types_function_returns = false +ij_typescript_prefer_explicit_types_vars_fields = false +ij_typescript_prefer_parameters_wrap = false +ij_typescript_property_prefix = +ij_typescript_reformat_c_style_comments = false +ij_typescript_space_after_colon = true +ij_typescript_space_after_comma = true +ij_typescript_space_after_dots_in_rest_parameter = false +ij_typescript_space_after_generator_mult = true +ij_typescript_space_after_property_colon = true +ij_typescript_space_after_quest = true +ij_typescript_space_after_type_colon = true +ij_typescript_space_after_unary_not = false +ij_typescript_space_before_async_arrow_lparen = true +ij_typescript_space_before_catch_keyword = true +ij_typescript_space_before_catch_left_brace = true +ij_typescript_space_before_catch_parentheses = true +ij_typescript_space_before_class_lbrace = true +ij_typescript_space_before_class_left_brace = true +ij_typescript_space_before_colon = true +ij_typescript_space_before_comma = false +ij_typescript_space_before_do_left_brace = true +ij_typescript_space_before_else_keyword = true +ij_typescript_space_before_else_left_brace = true +ij_typescript_space_before_finally_keyword = true +ij_typescript_space_before_finally_left_brace = true +ij_typescript_space_before_for_left_brace = true +ij_typescript_space_before_for_parentheses = true +ij_typescript_space_before_for_semicolon = false +ij_typescript_space_before_function_left_parenth = true +ij_typescript_space_before_generator_mult = false +ij_typescript_space_before_if_left_brace = true +ij_typescript_space_before_if_parentheses = true +ij_typescript_space_before_method_call_parentheses = false +ij_typescript_space_before_method_left_brace = true +ij_typescript_space_before_method_parentheses = false +ij_typescript_space_before_property_colon = false +ij_typescript_space_before_quest = true +ij_typescript_space_before_switch_left_brace = true +ij_typescript_space_before_switch_parentheses = true +ij_typescript_space_before_try_left_brace = true +ij_typescript_space_before_type_colon = false +ij_typescript_space_before_unary_not = false +ij_typescript_space_before_while_keyword = true +ij_typescript_space_before_while_left_brace = true +ij_typescript_space_before_while_parentheses = true +ij_typescript_spaces_around_additive_operators = true +ij_typescript_spaces_around_arrow_function_operator = true +ij_typescript_spaces_around_assignment_operators = true +ij_typescript_spaces_around_bitwise_operators = true +ij_typescript_spaces_around_equality_operators = true +ij_typescript_spaces_around_logical_operators = true +ij_typescript_spaces_around_multiplicative_operators = true +ij_typescript_spaces_around_relational_operators = true +ij_typescript_spaces_around_shift_operators = true +ij_typescript_spaces_around_unary_operator = false +ij_typescript_spaces_within_array_initializer_brackets = false +ij_typescript_spaces_within_brackets = false +ij_typescript_spaces_within_catch_parentheses = false +ij_typescript_spaces_within_for_parentheses = false +ij_typescript_spaces_within_if_parentheses = false +ij_typescript_spaces_within_imports = false +ij_typescript_spaces_within_interpolation_expressions = false +ij_typescript_spaces_within_method_call_parentheses = false +ij_typescript_spaces_within_method_parentheses = false +ij_typescript_spaces_within_object_literal_braces = false +ij_typescript_spaces_within_object_type_braces = true +ij_typescript_spaces_within_parentheses = false +ij_typescript_spaces_within_switch_parentheses = false +ij_typescript_spaces_within_type_assertion = false +ij_typescript_spaces_within_union_types = true +ij_typescript_spaces_within_while_parentheses = false +ij_typescript_special_else_if_treatment = true +ij_typescript_ternary_operation_signs_on_next_line = false +ij_typescript_ternary_operation_wrap = normal +ij_typescript_union_types_wrap = on_every_item +ij_typescript_use_chained_calls_group_indents = false +ij_typescript_use_double_quotes = true +ij_typescript_use_explicit_js_extension = auto +ij_typescript_use_path_mapping = always +ij_typescript_use_public_modifier = false +ij_typescript_use_semicolon_after_statement = true +ij_typescript_var_declaration_wrap = normal +ij_typescript_while_brace_force = always +ij_typescript_while_on_new_line = false +ij_typescript_wrap_comments = true + +[{*.bash,*.sh,*.zsh}] +indent_size = 2 +tab_width = 2 +ij_shell_binary_ops_start_line = false +ij_shell_keep_column_alignment_padding = false +ij_shell_minify_program = false +ij_shell_redirect_followed_by_space = false +ij_shell_switch_cases_indented = false +ij_shell_use_unix_line_separator = true + +[{*.cjs,*.js}] +ij_javascript_align_imports = false +ij_javascript_align_multiline_array_initializer_expression = false +ij_javascript_align_multiline_binary_operation = false +ij_javascript_align_multiline_chained_methods = false +ij_javascript_align_multiline_extends_list = false +ij_javascript_align_multiline_for = false +ij_javascript_align_multiline_parameters = false +ij_javascript_align_multiline_parameters_in_calls = false +ij_javascript_align_multiline_ternary_operation = false +ij_javascript_align_object_properties = 0 +ij_javascript_align_union_types = false +ij_javascript_align_var_statements = 0 +ij_javascript_array_initializer_new_line_after_left_brace = false +ij_javascript_array_initializer_right_brace_on_new_line = false +ij_javascript_array_initializer_wrap = normal +ij_javascript_assignment_wrap = normal +ij_javascript_binary_operation_sign_on_next_line = false +ij_javascript_binary_operation_wrap = normal +ij_javascript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** +ij_javascript_blank_lines_after_imports = 1 +ij_javascript_blank_lines_around_class = 1 +ij_javascript_blank_lines_around_field = 0 +ij_javascript_blank_lines_around_function = 1 +ij_javascript_blank_lines_around_method = 1 +ij_javascript_block_brace_style = end_of_line +ij_javascript_block_comment_add_space = false +ij_javascript_block_comment_at_first_column = true +ij_javascript_call_parameters_new_line_after_left_paren = false +ij_javascript_call_parameters_right_paren_on_new_line = false +ij_javascript_call_parameters_wrap = normal +ij_javascript_catch_on_new_line = false +ij_javascript_chained_call_dot_on_new_line = true +ij_javascript_class_brace_style = end_of_line +ij_javascript_comma_on_new_line = false +ij_javascript_do_while_brace_force = always +ij_javascript_else_on_new_line = false +ij_javascript_enforce_trailing_comma = remove +ij_javascript_extends_keyword_wrap = normal +ij_javascript_extends_list_wrap = normal +ij_javascript_field_prefix = _ +ij_javascript_file_name_style = relaxed +ij_javascript_finally_on_new_line = false +ij_javascript_for_brace_force = always +ij_javascript_for_statement_new_line_after_left_paren = false +ij_javascript_for_statement_right_paren_on_new_line = false +ij_javascript_for_statement_wrap = normal +ij_javascript_force_quote_style = true +ij_javascript_force_semicolon_style = true +ij_javascript_function_expression_brace_style = end_of_line +ij_javascript_if_brace_force = always +ij_javascript_import_merge_members = global +ij_javascript_import_prefer_absolute_path = global +ij_javascript_import_sort_members = true +ij_javascript_import_sort_module_name = false +ij_javascript_import_use_node_resolution = true +ij_javascript_imports_wrap = normal +ij_javascript_indent_case_from_switch = true +ij_javascript_indent_chained_calls = true +ij_javascript_indent_package_children = 0 +ij_javascript_jsx_attribute_value = braces +ij_javascript_keep_blank_lines_in_code = 1 +ij_javascript_keep_first_column_comment = true +ij_javascript_keep_indents_on_empty_lines = false +ij_javascript_keep_line_breaks = true +ij_javascript_keep_simple_blocks_in_one_line = true +ij_javascript_keep_simple_methods_in_one_line = true +ij_javascript_line_comment_add_space = true +ij_javascript_line_comment_at_first_column = false +ij_javascript_method_brace_style = end_of_line +ij_javascript_method_call_chain_wrap = normal +ij_javascript_method_parameters_new_line_after_left_paren = false +ij_javascript_method_parameters_right_paren_on_new_line = false +ij_javascript_method_parameters_wrap = normal +ij_javascript_object_literal_wrap = on_every_item +ij_javascript_object_types_wrap = on_every_item +ij_javascript_parentheses_expression_new_line_after_left_paren = false +ij_javascript_parentheses_expression_right_paren_on_new_line = false +ij_javascript_place_assignment_sign_on_next_line = false +ij_javascript_prefer_as_type_cast = false +ij_javascript_prefer_explicit_types_function_expression_returns = false +ij_javascript_prefer_explicit_types_function_returns = false +ij_javascript_prefer_explicit_types_vars_fields = false +ij_javascript_prefer_parameters_wrap = false +ij_javascript_property_prefix = +ij_javascript_reformat_c_style_comments = false +ij_javascript_space_after_colon = true +ij_javascript_space_after_comma = true +ij_javascript_space_after_dots_in_rest_parameter = false +ij_javascript_space_after_generator_mult = true +ij_javascript_space_after_property_colon = true +ij_javascript_space_after_quest = true +ij_javascript_space_after_type_colon = true +ij_javascript_space_after_unary_not = false +ij_javascript_space_before_async_arrow_lparen = true +ij_javascript_space_before_catch_keyword = true +ij_javascript_space_before_catch_left_brace = true +ij_javascript_space_before_catch_parentheses = true +ij_javascript_space_before_class_lbrace = true +ij_javascript_space_before_class_left_brace = true +ij_javascript_space_before_colon = true +ij_javascript_space_before_comma = false +ij_javascript_space_before_do_left_brace = true +ij_javascript_space_before_else_keyword = true +ij_javascript_space_before_else_left_brace = true +ij_javascript_space_before_finally_keyword = true +ij_javascript_space_before_finally_left_brace = true +ij_javascript_space_before_for_left_brace = true +ij_javascript_space_before_for_parentheses = true +ij_javascript_space_before_for_semicolon = false +ij_javascript_space_before_function_left_parenth = true +ij_javascript_space_before_generator_mult = false +ij_javascript_space_before_if_left_brace = true +ij_javascript_space_before_if_parentheses = true +ij_javascript_space_before_method_call_parentheses = false +ij_javascript_space_before_method_left_brace = true +ij_javascript_space_before_method_parentheses = false +ij_javascript_space_before_property_colon = false +ij_javascript_space_before_quest = true +ij_javascript_space_before_switch_left_brace = true +ij_javascript_space_before_switch_parentheses = true +ij_javascript_space_before_try_left_brace = true +ij_javascript_space_before_type_colon = false +ij_javascript_space_before_unary_not = false +ij_javascript_space_before_while_keyword = true +ij_javascript_space_before_while_left_brace = true +ij_javascript_space_before_while_parentheses = true +ij_javascript_spaces_around_additive_operators = true +ij_javascript_spaces_around_arrow_function_operator = true +ij_javascript_spaces_around_assignment_operators = true +ij_javascript_spaces_around_bitwise_operators = true +ij_javascript_spaces_around_equality_operators = true +ij_javascript_spaces_around_logical_operators = true +ij_javascript_spaces_around_multiplicative_operators = true +ij_javascript_spaces_around_relational_operators = true +ij_javascript_spaces_around_shift_operators = true +ij_javascript_spaces_around_unary_operator = false +ij_javascript_spaces_within_array_initializer_brackets = false +ij_javascript_spaces_within_brackets = false +ij_javascript_spaces_within_catch_parentheses = false +ij_javascript_spaces_within_for_parentheses = false +ij_javascript_spaces_within_if_parentheses = false +ij_javascript_spaces_within_imports = false +ij_javascript_spaces_within_interpolation_expressions = false +ij_javascript_spaces_within_method_call_parentheses = false +ij_javascript_spaces_within_method_parentheses = false +ij_javascript_spaces_within_object_literal_braces = false +ij_javascript_spaces_within_object_type_braces = true +ij_javascript_spaces_within_parentheses = false +ij_javascript_spaces_within_switch_parentheses = false +ij_javascript_spaces_within_type_assertion = false +ij_javascript_spaces_within_union_types = true +ij_javascript_spaces_within_while_parentheses = false +ij_javascript_special_else_if_treatment = true +ij_javascript_ternary_operation_signs_on_next_line = false +ij_javascript_ternary_operation_wrap = normal +ij_javascript_union_types_wrap = on_every_item +ij_javascript_use_chained_calls_group_indents = false +ij_javascript_use_double_quotes = true +ij_javascript_use_explicit_js_extension = auto +ij_javascript_use_path_mapping = always +ij_javascript_use_public_modifier = false +ij_javascript_use_semicolon_after_statement = true +ij_javascript_var_declaration_wrap = normal +ij_javascript_while_brace_force = always +ij_javascript_while_on_new_line = false +ij_javascript_wrap_comments = true + +[{*.ft,*.vm,*.vsl}] +ij_vtl_keep_indents_on_empty_lines = false + +[{*.gant,*.groovy,*.gy}] ij_groovy_align_group_field_declarations = false ij_groovy_align_multiline_array_initializer_expression = false ij_groovy_align_multiline_assignment = false ij_groovy_align_multiline_binary_operation = false ij_groovy_align_multiline_chained_methods = false ij_groovy_align_multiline_extends_list = false -ij_groovy_align_multiline_for = true +ij_groovy_align_multiline_for = false +ij_groovy_align_multiline_list_or_map = false ij_groovy_align_multiline_method_parentheses = false -ij_groovy_align_multiline_parameters = true +ij_groovy_align_multiline_parameters = false ij_groovy_align_multiline_parameters_in_calls = false -ij_groovy_align_multiline_resources = true +ij_groovy_align_multiline_resources = false ij_groovy_align_multiline_ternary_operation = false ij_groovy_align_multiline_throws_list = false +ij_groovy_align_named_args_in_map = false ij_groovy_align_throws_keyword = false ij_groovy_array_initializer_new_line_after_left_brace = false ij_groovy_array_initializer_right_brace_on_new_line = false -ij_groovy_array_initializer_wrap = off -ij_groovy_assert_statement_wrap = off -ij_groovy_assignment_wrap = off -ij_groovy_binary_operation_wrap = off +ij_groovy_array_initializer_wrap = normal +ij_groovy_assert_statement_wrap = normal +ij_groovy_assignment_wrap = normal +ij_groovy_binary_operation_wrap = normal ij_groovy_blank_lines_after_class_header = 0 ij_groovy_blank_lines_after_imports = 1 ij_groovy_blank_lines_after_package = 1 @@ -478,55 +848,75 @@ ij_groovy_blank_lines_before_imports = 1 ij_groovy_blank_lines_before_method_body = 0 ij_groovy_blank_lines_before_package = 0 ij_groovy_block_brace_style = end_of_line +ij_groovy_block_comment_add_space = false ij_groovy_block_comment_at_first_column = true ij_groovy_call_parameters_new_line_after_left_paren = false ij_groovy_call_parameters_right_paren_on_new_line = false -ij_groovy_call_parameters_wrap = off +ij_groovy_call_parameters_wrap = normal ij_groovy_catch_on_new_line = false ij_groovy_class_annotation_wrap = split_into_lines ij_groovy_class_brace_style = end_of_line -ij_groovy_do_while_brace_force = never +ij_groovy_class_count_to_use_import_on_demand = 999 +ij_groovy_do_while_brace_force = always ij_groovy_else_on_new_line = false -ij_groovy_enum_constants_wrap = off -ij_groovy_extends_keyword_wrap = off -ij_groovy_extends_list_wrap = off +ij_groovy_enable_groovydoc_formatting = true +ij_groovy_enum_constants_wrap = split_into_lines +ij_groovy_extends_keyword_wrap = normal +ij_groovy_extends_list_wrap = normal ij_groovy_field_annotation_wrap = split_into_lines ij_groovy_finally_on_new_line = false -ij_groovy_for_brace_force = never +ij_groovy_for_brace_force = always ij_groovy_for_statement_new_line_after_left_paren = false ij_groovy_for_statement_right_paren_on_new_line = false -ij_groovy_for_statement_wrap = off -ij_groovy_if_brace_force = never +ij_groovy_for_statement_wrap = normal +ij_groovy_ginq_general_clause_wrap_policy = 2 +ij_groovy_ginq_having_wrap_policy = 1 +ij_groovy_ginq_indent_having_clause = true +ij_groovy_ginq_indent_on_clause = true +ij_groovy_ginq_on_wrap_policy = 1 +ij_groovy_ginq_space_after_keyword = true +ij_groovy_if_brace_force = always +ij_groovy_import_annotation_wrap = 2 +ij_groovy_imports_layout = *,|,javax.**,java.**,|,$* ij_groovy_indent_case_from_switch = true -ij_groovy_keep_blank_lines_before_right_brace = 2 -ij_groovy_keep_blank_lines_in_code = 2 -ij_groovy_keep_blank_lines_in_declarations = 2 +ij_groovy_indent_label_blocks = true +ij_groovy_insert_inner_class_imports = false +ij_groovy_keep_blank_lines_before_right_brace = 1 +ij_groovy_keep_blank_lines_in_code = 1 +ij_groovy_keep_blank_lines_in_declarations = 1 ij_groovy_keep_control_statement_in_one_line = true ij_groovy_keep_first_column_comment = true ij_groovy_keep_indents_on_empty_lines = false ij_groovy_keep_line_breaks = true -ij_groovy_keep_multiple_expressions_in_one_line = false -ij_groovy_keep_simple_blocks_in_one_line = false +ij_groovy_keep_multiple_expressions_in_one_line = true +ij_groovy_keep_simple_blocks_in_one_line = true ij_groovy_keep_simple_classes_in_one_line = true ij_groovy_keep_simple_lambdas_in_one_line = true ij_groovy_keep_simple_methods_in_one_line = true +ij_groovy_label_indent_absolute = false +ij_groovy_label_indent_size = 0 ij_groovy_lambda_brace_style = end_of_line +ij_groovy_layout_static_imports_separately = true ij_groovy_line_comment_add_space = false +ij_groovy_line_comment_add_space_on_reformat = false ij_groovy_line_comment_at_first_column = true ij_groovy_method_annotation_wrap = split_into_lines ij_groovy_method_brace_style = end_of_line -ij_groovy_method_call_chain_wrap = off +ij_groovy_method_call_chain_wrap = normal ij_groovy_method_parameters_new_line_after_left_paren = false ij_groovy_method_parameters_right_paren_on_new_line = false -ij_groovy_method_parameters_wrap = off +ij_groovy_method_parameters_wrap = normal ij_groovy_modifier_list_wrap = false -ij_groovy_parameter_annotation_wrap = off +ij_groovy_names_count_to_use_import_on_demand = 999 +ij_groovy_packages_to_use_import_on_demand = * +ij_groovy_parameter_annotation_wrap = normal ij_groovy_parentheses_expression_new_line_after_left_paren = false ij_groovy_parentheses_expression_right_paren_on_new_line = false ij_groovy_prefer_parameters_wrap = false ij_groovy_resource_list_new_line_after_left_paren = false ij_groovy_resource_list_right_paren_on_new_line = false -ij_groovy_resource_list_wrap = off +ij_groovy_resource_list_wrap = normal +ij_groovy_space_after_assert_separator = true ij_groovy_space_after_colon = true ij_groovy_space_after_comma = true ij_groovy_space_after_comma_in_type_arguments = true @@ -535,10 +925,12 @@ ij_groovy_space_after_quest = true ij_groovy_space_after_type_cast = true ij_groovy_space_before_annotation_parameter_list = false ij_groovy_space_before_array_initializer_left_brace = false +ij_groovy_space_before_assert_separator = false ij_groovy_space_before_catch_keyword = true ij_groovy_space_before_catch_left_brace = true ij_groovy_space_before_catch_parentheses = true ij_groovy_space_before_class_left_brace = true +ij_groovy_space_before_closure_left_brace = true ij_groovy_space_before_colon = true ij_groovy_space_before_comma = false ij_groovy_space_before_do_left_brace = true @@ -555,6 +947,7 @@ ij_groovy_space_before_method_call_parentheses = false ij_groovy_space_before_method_left_brace = true ij_groovy_space_before_method_parentheses = false ij_groovy_space_before_quest = true +ij_groovy_space_before_record_parentheses = false ij_groovy_space_before_switch_left_brace = true ij_groovy_space_before_switch_parentheses = true ij_groovy_space_before_synchronized_left_brace = true @@ -564,6 +957,8 @@ ij_groovy_space_before_try_parentheses = true ij_groovy_space_before_while_keyword = true ij_groovy_space_before_while_left_brace = true ij_groovy_space_before_while_parentheses = true +ij_groovy_space_in_named_argument = true +ij_groovy_space_in_named_argument_before_colon = false ij_groovy_space_within_empty_array_initializer_braces = false ij_groovy_space_within_empty_method_call_parentheses = false ij_groovy_spaces_around_additive_operators = true @@ -573,6 +968,7 @@ ij_groovy_spaces_around_equality_operators = true ij_groovy_spaces_around_lambda_arrow = true ij_groovy_spaces_around_logical_operators = true ij_groovy_spaces_around_multiplicative_operators = true +ij_groovy_spaces_around_regex_operators = true ij_groovy_spaces_around_relational_operators = true ij_groovy_spaces_around_shift_operators = true ij_groovy_spaces_within_annotation_parentheses = false @@ -582,42 +978,110 @@ ij_groovy_spaces_within_brackets = false ij_groovy_spaces_within_cast_parentheses = false ij_groovy_spaces_within_catch_parentheses = false ij_groovy_spaces_within_for_parentheses = false +ij_groovy_spaces_within_gstring_injection_braces = false ij_groovy_spaces_within_if_parentheses = false +ij_groovy_spaces_within_list_or_map = false ij_groovy_spaces_within_method_call_parentheses = false ij_groovy_spaces_within_method_parentheses = false ij_groovy_spaces_within_parentheses = false ij_groovy_spaces_within_switch_parentheses = false ij_groovy_spaces_within_synchronized_parentheses = false ij_groovy_spaces_within_try_parentheses = false +ij_groovy_spaces_within_tuple_expression = false ij_groovy_spaces_within_while_parentheses = false ij_groovy_special_else_if_treatment = true -ij_groovy_ternary_operation_wrap = off -ij_groovy_throws_keyword_wrap = off -ij_groovy_throws_list_wrap = off -ij_groovy_variable_annotation_wrap = off -ij_groovy_while_brace_force = never +ij_groovy_ternary_operation_wrap = normal +ij_groovy_throws_keyword_wrap = normal +ij_groovy_throws_list_wrap = normal +ij_groovy_use_flying_geese_braces = false +ij_groovy_use_fq_class_names = false +ij_groovy_use_fq_class_names_in_javadoc = false +ij_groovy_use_relative_indents = false +ij_groovy_use_single_class_imports = true +ij_groovy_variable_annotation_wrap = normal +ij_groovy_while_brace_force = always ij_groovy_while_on_new_line = false +ij_groovy_wrap_chain_calls_after_dot = false ij_groovy_wrap_long_lines = false -[{*.jhm,*.xslt,*.xul,*.tagx,*.rng,*.xsl,*.xsd,*.jspx,*.ant,*.xml,*.tld,*.fxml,*.wsdl,*.jrxml,*.jnlp,*.pom}] -ij_xml_block_comment_at_first_column = true -ij_xml_keep_indents_on_empty_lines = false -ij_xml_line_comment_at_first_column = true +[{*.har,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,bowerrc,jest.config}] +indent_size = 2 +ij_json_array_wrapping = split_into_lines +ij_json_keep_blank_lines_in_code = 0 +ij_json_keep_indents_on_empty_lines = false +ij_json_keep_line_breaks = false +ij_json_keep_trailing_comma = false +ij_json_object_wrapping = split_into_lines +ij_json_property_alignment = do_not_align +ij_json_space_after_colon = true +ij_json_space_after_comma = true +ij_json_space_before_colon = false +ij_json_space_before_comma = false +ij_json_spaces_within_braces = false +ij_json_spaces_within_brackets = false +ij_json_wrap_long_lines = false + +[{*.htm,*.html,*.ng,*.peb,*.sht,*.shtm,*.shtml}] +ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3 +ij_html_align_attributes = true +ij_html_align_text = false +ij_html_attribute_wrap = normal +ij_html_block_comment_add_space = false +ij_html_block_comment_at_first_column = true +ij_html_do_not_align_children_of_min_lines = 0 +ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p +ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot +ij_html_enforce_quotes = true +ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var +ij_html_keep_blank_lines = 1 +ij_html_keep_indents_on_empty_lines = false +ij_html_keep_line_breaks = true +ij_html_keep_line_breaks_in_text = true +ij_html_keep_whitespaces = false +ij_html_keep_whitespaces_inside = span,pre,textarea +ij_html_line_comment_at_first_column = true +ij_html_new_line_after_last_attribute = never +ij_html_new_line_before_first_attribute = never +ij_html_quote_style = double +ij_html_remove_new_line_before_tags = br +ij_html_space_after_tag_name = false +ij_html_space_around_equality_in_attribute = false +ij_html_space_inside_empty_tag = false +ij_html_text_wrap = normal + +[{*.http,*.rest}] +indent_size = 0 +ij_continuation_indent_size = 4 +ij_http-request_call_parameters_wrap = normal +ij_http-request_method_parameters_wrap = split_into_lines +ij_http-request_space_before_comma = true +ij_http-request_spaces_around_assignment_operators = true + +[{*.jsf,*.jsp,*.jspf,*.tag,*.tagf,*.xjsp}] +ij_jsp_jsp_prefer_comma_separated_import_list = false +ij_jsp_keep_indents_on_empty_lines = false + +[{*.jspx,*.tagx}] +ij_jspx_keep_indents_on_empty_lines = false [{*.kt,*.kts}] ij_kotlin_align_in_columns_case_branch = false ij_kotlin_align_multiline_binary_operation = false ij_kotlin_align_multiline_extends_list = false ij_kotlin_align_multiline_method_parentheses = false -ij_kotlin_align_multiline_parameters = true +ij_kotlin_align_multiline_parameters = false ij_kotlin_align_multiline_parameters_in_calls = false -ij_kotlin_assignment_wrap = off +ij_kotlin_allow_trailing_comma = false +ij_kotlin_allow_trailing_comma_on_call_site = false +ij_kotlin_assignment_wrap = normal ij_kotlin_blank_lines_after_class_header = 0 ij_kotlin_blank_lines_around_block_when_branches = 0 +ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 +ij_kotlin_block_comment_add_space = false ij_kotlin_block_comment_at_first_column = true ij_kotlin_call_parameters_new_line_after_left_paren = false ij_kotlin_call_parameters_right_paren_on_new_line = false -ij_kotlin_call_parameters_wrap = off +ij_kotlin_call_parameters_wrap = normal ij_kotlin_catch_on_new_line = false ij_kotlin_class_annotation_wrap = split_into_lines ij_kotlin_continuation_indent_for_chained_calls = true @@ -628,30 +1092,34 @@ ij_kotlin_continuation_indent_in_if_conditions = true ij_kotlin_continuation_indent_in_parameter_lists = true ij_kotlin_continuation_indent_in_supertype_lists = true ij_kotlin_else_on_new_line = false -ij_kotlin_enum_constants_wrap = off -ij_kotlin_extends_list_wrap = off +ij_kotlin_enum_constants_wrap = split_into_lines +ij_kotlin_extends_list_wrap = normal ij_kotlin_field_annotation_wrap = split_into_lines ij_kotlin_finally_on_new_line = false ij_kotlin_if_rparen_on_new_line = false ij_kotlin_import_nested_classes = false +ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^ ij_kotlin_insert_whitespaces_in_simple_one_line_method = true -ij_kotlin_keep_blank_lines_before_right_brace = 2 -ij_kotlin_keep_blank_lines_in_code = 2 -ij_kotlin_keep_blank_lines_in_declarations = 2 +ij_kotlin_keep_blank_lines_before_right_brace = 1 +ij_kotlin_keep_blank_lines_in_code = 1 +ij_kotlin_keep_blank_lines_in_declarations = 1 ij_kotlin_keep_first_column_comment = true ij_kotlin_keep_indents_on_empty_lines = false ij_kotlin_keep_line_breaks = true ij_kotlin_lbrace_on_next_line = false +ij_kotlin_line_break_after_multiline_when_entry = true ij_kotlin_line_comment_add_space = false +ij_kotlin_line_comment_add_space_on_reformat = false ij_kotlin_line_comment_at_first_column = true ij_kotlin_method_annotation_wrap = split_into_lines -ij_kotlin_method_call_chain_wrap = off +ij_kotlin_method_call_chain_wrap = normal ij_kotlin_method_parameters_new_line_after_left_paren = false ij_kotlin_method_parameters_right_paren_on_new_line = false -ij_kotlin_method_parameters_wrap = off -ij_kotlin_name_count_to_use_star_import = 5 -ij_kotlin_name_count_to_use_star_import_for_members = 3 -ij_kotlin_parameter_annotation_wrap = off +ij_kotlin_method_parameters_wrap = normal +ij_kotlin_name_count_to_use_star_import = 2147483647 +ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 +ij_kotlin_packages_to_use_import_on_demand = +ij_kotlin_parameter_annotation_wrap = normal ij_kotlin_space_after_comma = true ij_kotlin_space_after_extend_colon = true ij_kotlin_space_after_type_colon = true @@ -674,48 +1142,65 @@ ij_kotlin_spaces_around_range = false ij_kotlin_spaces_around_relational_operators = true ij_kotlin_spaces_around_unary_operator = false ij_kotlin_spaces_around_when_arrow = true -ij_kotlin_variable_annotation_wrap = off +ij_kotlin_variable_annotation_wrap = normal ij_kotlin_while_on_new_line = false ij_kotlin_wrap_elvis_expressions = 1 -ij_kotlin_wrap_expression_body_functions = 0 +ij_kotlin_wrap_expression_body_functions = 1 ij_kotlin_wrap_first_method_in_call_chain = false -[{*.shtm,*.htm,*.sht,*.shtml,*.html}] -ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3 -ij_html_align_attributes = true -ij_html_align_text = false -ij_html_attribute_wrap = normal -ij_html_block_comment_at_first_column = true -ij_html_do_not_align_children_of_min_lines = 0 -ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p -ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot -ij_html_enforce_quotes = false -ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var -ij_html_keep_blank_lines = 2 -ij_html_keep_indents_on_empty_lines = false -ij_html_keep_line_breaks = true -ij_html_keep_line_breaks_in_text = true -ij_html_keep_whitespaces = false -ij_html_keep_whitespaces_inside = span,pre,textarea -ij_html_line_comment_at_first_column = true -ij_html_new_line_after_last_attribute = never -ij_html_new_line_before_first_attribute = never -ij_html_quote_style = double -ij_html_remove_new_line_before_tags = br -ij_html_space_after_tag_name = false -ij_html_space_around_equality_in_attribute = false -ij_html_space_inside_empty_tag = false -ij_html_text_wrap = normal +[{*.markdown,*.md}] +ij_markdown_force_one_space_after_blockquote_symbol = true +ij_markdown_force_one_space_after_header_symbol = true +ij_markdown_force_one_space_after_list_bullet = true +ij_markdown_force_one_space_between_words = true +ij_markdown_format_tables = true +ij_markdown_insert_quote_arrows_on_wrap = true +ij_markdown_keep_indents_on_empty_lines = false +ij_markdown_keep_line_breaks_inside_text_blocks = true +ij_markdown_max_lines_around_block_elements = 1 +ij_markdown_max_lines_around_header = 1 +ij_markdown_max_lines_between_paragraphs = 1 +ij_markdown_min_lines_around_block_elements = 1 +ij_markdown_min_lines_around_header = 1 +ij_markdown_min_lines_between_paragraphs = 1 +ij_markdown_wrap_text_if_long = true +ij_markdown_wrap_text_inside_blockquotes = true -[{*.yml,*.yaml}] +[{*.pb,*.textproto}] indent_size = 2 -ij_continuation_indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 4 +ij_prototext_keep_blank_lines_in_code = 2 +ij_prototext_keep_indents_on_empty_lines = false +ij_prototext_keep_line_breaks = true +ij_prototext_space_after_colon = true +ij_prototext_space_after_comma = true +ij_prototext_space_before_colon = false +ij_prototext_space_before_comma = false +ij_prototext_spaces_within_braces = true +ij_prototext_spaces_within_brackets = false + +[{*.properties,spring.handlers,spring.schemas}] +ij_properties_align_group_field_declarations = false +ij_properties_keep_blank_lines = false +ij_properties_key_value_delimiter = equals +ij_properties_spaces_around_key_value_delimiter = false + +[{*.qute.htm,*.qute.html,*.qute.json,*.qute.txt,*.qute.yaml,*.qute.yml}] +ij_qute_keep_indents_on_empty_lines = false + +[{*.toml,Cargo.lock,Cargo.toml.orig,Gopkg.lock,Pipfile,poetry.lock}] +ij_toml_keep_indents_on_empty_lines = false + +[{*.yaml,*.yml}] +indent_size = 2 +ij_yaml_align_values_properties = do_not_align +ij_yaml_autoinsert_sequence_marker = true +ij_yaml_block_mapping_on_new_line = false +ij_yaml_indent_sequence_value = true ij_yaml_keep_indents_on_empty_lines = false ij_yaml_keep_line_breaks = true - -[{*.zsh,*.bash,*.sh}] -ij_shell_binary_ops_start_line = false -ij_shell_keep_column_alignment_padding = false -ij_shell_minify_program = false -ij_shell_redirect_followed_by_space = false -ij_shell_switch_cases_indented = false +ij_yaml_sequence_on_new_line = false +ij_yaml_space_before_colon = false +ij_yaml_spaces_within_braces = true +ij_yaml_spaces_within_brackets = true diff --git a/README.md b/README.md index a6c51d49..0ec10a33 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,6 @@ AlpacaAPI alpacaAPI = new AlpacaAPI(oAuthToken); AlpacaAPI alpacaAPI = AlpacaAPI.builder() .withEndpointAPIType(EndpointAPIType.PAPER) .build(); - ``` Note that this library uses [OkHttp](https://square.github.io/okhttp/) as its HTTP client library which creates background threads to service requests. These threads persist even if the main thread exists so if you want to destroy these threads when you're done using [`AlpacaAPI`](src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java) so your program can exit without calling `System.exit()`, use the following snippet: From 92656f777c005302bb3245bd0a38e6ecfa6918ca Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Sun, 20 Aug 2023 16:37:54 -0600 Subject: [PATCH 50/84] Update to 9.2.0 --- README.md | 4 ++-- build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0ec10a33..82630ee6 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ If you are using Gradle as your build tool, add the following dependency to your ``` dependencies { - implementation group: 'net.jacobpeterson', name: 'alpaca-java', version: '9.1.4' + implementation group: 'net.jacobpeterson', name: 'alpaca-java', version: '9.2.0' } ``` @@ -27,7 +27,7 @@ If you are using Maven as your build tool, add the following dependency to your net.jacobpeterson alpaca-java - 9.1.4 + 9.2.0 compile ``` diff --git a/build.gradle b/build.gradle index a40691fa..a861d055 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ apply plugin: 'jsonschema2pojo' archivesBaseName = 'alpaca-java' group = 'net.jacobpeterson' -version = '9.1.4-SNAPSHOT' +version = '9.2.0-SNAPSHOT' repositories { mavenCentral() From 5d1f23126fb83cec4630630698fcba92fc367d56 Mon Sep 17 00:00:00 2001 From: Jc Date: Tue, 7 Nov 2023 16:19:17 -0500 Subject: [PATCH 51/84] Fix --- .../alpaca/rest/endpoint/orders/OrdersEndpoint.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/orders/OrdersEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/orders/OrdersEndpoint.java index 0ad4a66e..b2a0563d 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/orders/OrdersEndpoint.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/orders/OrdersEndpoint.java @@ -251,7 +251,7 @@ public Order requestMarketOrder(String symbol, Integer quantity, OrderSide side, */ public Order requestFractionalMarketOrder(String symbol, Double quantity, OrderSide side) throws AlpacaClientException { - return requestOrder(symbol, quantity, null, side, OrderType.MARKET, OrderTimeInForce.GOOD_UNTIL_CANCELLED, null, + return requestOrder(symbol, quantity, null, side, OrderType.MARKET, OrderTimeInForce.DAY, null, null, null, null, null, null, null, null, null, null); } From 8d9ca1434d3bb5f29ca68eac16267d788021140e Mon Sep 17 00:00:00 2001 From: gretard Date: Tue, 2 Jan 2024 21:00:58 +0200 Subject: [PATCH 52/84] Add corporate actions endpoint for #118 --- build.gradle | 1 + .../common/announcement.json | 53 ++++++++ .../net/jacobpeterson/alpaca/AlpacaAPI.java | 9 ++ .../CorporateActionsEndpoint.java | 121 ++++++++++++++++++ .../alpaca/test/mock/AlpacaAPITest.java | 13 ++ .../CorporateActionsEndpointTest.java | 104 +++++++++++++++ 6 files changed, 301 insertions(+) create mode 100644 schema_json/endpoint/corporate_actions/common/announcement.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/rest/endpoint/corporateactions/CorporateActionsEndpoint.java create mode 100644 src/test/java/net/jacobpeterson/alpaca/test/mock/corporateactions/CorporateActionsEndpointTest.java diff --git a/build.gradle b/build.gradle index a861d055..7c65cc3d 100644 --- a/build.gradle +++ b/build.gradle @@ -53,6 +53,7 @@ dependencies { testImplementation group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.36' testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter', version: '5.8.2' testImplementation group: 'org.mockito', name: 'mockito-core', version: '4.6.1' + testImplementation group: 'com.github.gmazzo.okhttp.mock', name: 'mock-client', version: '2.0.0' } // Exclude any SLF4j-implementation transitive dependencies so users can use a logging framework of their choice diff --git a/schema_json/endpoint/corporate_actions/common/announcement.json b/schema_json/endpoint/corporate_actions/common/announcement.json new file mode 100644 index 00000000..14c8e66e --- /dev/null +++ b/schema_json/endpoint/corporate_actions/common/announcement.json @@ -0,0 +1,53 @@ +{ + "type": "object", + "title": "See Corporate announcement.", + "properties": { + "id": { + "existingJavaType": "java.lang.String" + }, + "corporate_actions_id": { + "existingJavaType": "java.lang.String" + }, + "ca_type": { + "existingJavaType": "java.lang.String" + }, + "ca_sub_type": { + "existingJavaType": "java.lang.String" + }, + "initiating_symbol": { + "existingJavaType": "java.lang.String" + }, + "initiating_original_cusip": { + "existingJavaType": "java.lang.String" + }, + "target_symbol": { + "target_symbol": "java.lang.String" + }, + "target_original_cusip": { + "target_original_cusip": "java.lang.String" + }, + "declaration_date": { + "declaration_date": "java.lang.String" + }, + "expiration_date": { + "declaration_date": "java.lang.String" + }, + "record_date": { + "declaration_date": "java.lang.String" + }, + "payable_date": { + "declaration_date": "java.lang.String" + }, + "cash": { + "declaration_date": "java.lang.String" + }, + "old_rate": { + "declaration_date": "java.lang.String" + }, + "new_rate": { + "declaration_date": "java.lang.String" + } + + + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java index d1e75376..922ddd10 100644 --- a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java +++ b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java @@ -12,6 +12,7 @@ import net.jacobpeterson.alpaca.rest.endpoint.assets.AssetsEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.calendar.CalendarEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.clock.ClockEndpoint; +import net.jacobpeterson.alpaca.rest.endpoint.corporateactions.CorporateActionsEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.marketdata.crypto.CryptoMarketDataEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.marketdata.stock.StockMarketDataEndpoint; import net.jacobpeterson.alpaca.rest.endpoint.orders.OrdersEndpoint; @@ -67,6 +68,7 @@ public class AlpacaAPI { private final CryptoMarketDataWebsocket cryptoMarketDataWebsocket; private final StockMarketDataWebsocket stockMarketDataWebsocket; + private final CorporateActionsEndpoint corporateActionsEndpoint; /** * Instantiates a new {@link AlpacaAPI} using properties specified in alpaca.properties file (or their * associated defaults). @@ -197,6 +199,7 @@ public AlpacaAPI(OkHttpClient okHttpClient, String keyID, String secretKey, Stri new CryptoMarketDataWebsocket(okHttpClient, keyID, secretKey); stockMarketDataWebsocket = stockDataClient == null ? null : new StockMarketDataWebsocket(okHttpClient, dataAPIType, keyID, secretKey); + corporateActionsEndpoint = new CorporateActionsEndpoint(brokerClient); } /** @@ -320,6 +323,12 @@ public AlpacaClient getStockDataClient() { return stockDataClient; } + /** + * @return the {@link CorporateActionsEndpoint} + */ + public CorporateActionsEndpoint corporateActions() { + return corporateActionsEndpoint; + } /** * Creates a {@link Builder} for {@link AlpacaAPI}. * diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/corporateactions/CorporateActionsEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/corporateactions/CorporateActionsEndpoint.java new file mode 100644 index 00000000..f467c4f0 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/corporateactions/CorporateActionsEndpoint.java @@ -0,0 +1,121 @@ +package net.jacobpeterson.alpaca.rest.endpoint.corporateactions; + +import com.google.gson.reflect.TypeToken; +import net.jacobpeterson.alpaca.model.endpoint.corporateactions.common.Announcement; +import net.jacobpeterson.alpaca.rest.AlpacaClient; +import net.jacobpeterson.alpaca.rest.AlpacaClientException; +import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; +import okhttp3.HttpUrl; +import okhttp3.Request; + +import java.lang.reflect.Type; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * {@link AlpacaEndpoint} for + * Corporate actions + * Endpoint. + */ +public class CorporateActionsEndpoint extends AlpacaEndpoint { + + private static final Type ANNOUNCEMENT_ARRAYLIST_TYPE = new TypeToken>() {}.getType(); + + private static final List ALL_CA_TYPES = Arrays.asList("dividend", "merger", "spinoff", "split"); + + /** + * Instantiates a new {@link AlpacaEndpoint}. + * + * @param alpacaClient the {@link AlpacaClient} + */ + public CorporateActionsEndpoint(AlpacaClient alpacaClient) { + super(alpacaClient, "corporate_actions/announcements"); + } + + /** + * Gets a {@link Announcement} of the requested id. + * @param id - announcement id to query for + * @return the {@link Announcement} + * @throws AlpacaClientException thrown for {@link AlpacaClientException}s + */ + public Announcement getAnnouncement(String id) throws AlpacaClientException { + checkNotNull(id); + HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() + .addPathSegment(endpointPathSegment) + .addPathSegment(id); + Request request = alpacaClient.requestBuilder(urlBuilder.build()) + .get() + .build(); + return alpacaClient.requestObject(request, Announcement.class); + + } + + /** + * Gets list of all {@link Announcement} within last 90 days + * @return a {@link List} of {@link Announcement} + * @throws AlpacaClientException thrown for {@link AlpacaClientException}s + */ + public List getAnnouncements() throws AlpacaClientException { + return this.getAnnouncements(ALL_CA_TYPES, LocalDate.now().minusDays(90), LocalDate.now(), null, null, null); + } + + /** + * Gets list of all {@link Announcement} within last 90 days for specific symbol + * @param symbol the symbol to query for + * @return a {@link List} of {@link Announcement} + * @throws AlpacaClientException thrown for {@link AlpacaClientException}s + */ + public List getAnnouncements(String symbol) throws AlpacaClientException { + return this.getAnnouncements(ALL_CA_TYPES, LocalDate.now().minusDays(90), LocalDate.now(), symbol, null, null); + } + + /** + * Gets list of all {@link Announcement} for specific query + * @param caTypes - a list of Dividend, Merger, Spinoff, or Split + * @param since - the start (inclusive) of the date range when searching corporate action announcements. This should follow the YYYY-MM-DD format. The date range is limited to 90 days + * @param until - the end (inclusive) of the date range when searching corporate action announcements. This should follow the YYYY-MM-DD format. The date range is limited to 90 days + * @param symbol - the symbol of the company initiating the announcement + * @param cusip - the CUSIP of the company initiating the announcement + * @param dateType - one of declaration_date, ex_date, record_date, or payable_date + * @return a {@link List} of {@link Announcement} + * @throws AlpacaClientException thrown for {@link AlpacaClientException}s + */ + public List getAnnouncements( + List caTypes, LocalDate since, LocalDate until, + String symbol, + String cusip, + String dateType + ) + throws AlpacaClientException { + HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() + .addPathSegment(endpointPathSegment); + checkNotNull(caTypes); + checkNotNull(since); + checkNotNull(until); + + + urlBuilder.addQueryParameter("ca_types", String.join(",", caTypes)); + + urlBuilder.addQueryParameter("since", since.toString()); + urlBuilder.addQueryParameter("until", until.toString()); + + if (symbol != null) { + urlBuilder.addQueryParameter("symbol", symbol); + } + if (cusip != null) { + urlBuilder.addQueryParameter("cusip", cusip); + } + if (dateType !=null) { + urlBuilder.addQueryParameter("date_type", dateType); + } + Request request = alpacaClient.requestBuilder(urlBuilder.build()) + .get() + .build(); + return alpacaClient.requestObject(request, ANNOUNCEMENT_ARRAYLIST_TYPE); + + } +} diff --git a/src/test/java/net/jacobpeterson/alpaca/test/mock/AlpacaAPITest.java b/src/test/java/net/jacobpeterson/alpaca/test/mock/AlpacaAPITest.java index d5f4f320..0fbc722a 100644 --- a/src/test/java/net/jacobpeterson/alpaca/test/mock/AlpacaAPITest.java +++ b/src/test/java/net/jacobpeterson/alpaca/test/mock/AlpacaAPITest.java @@ -6,6 +6,8 @@ import okhttp3.OkHttpClient; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertNotNull; + /** * {@link AlpacaAPITest} tests {@link AlpacaAPI} using mocked objects with Mockito. */ @@ -71,4 +73,15 @@ public void testAlpacaAPIConstructor_okHttpClient_keyID_secret_oAuthToken_endpoi new AlpacaAPI(okHttpClient, null, null, oAuthToken, EndpointAPIType.PAPER, DataAPIType.IEX); new AlpacaAPI(okHttpClient, keyID, secret, null, EndpointAPIType.LIVE, DataAPIType.SIP); } + + @Test + public void testAlpacaAPIConstructor_corporateActionsEndpoint() { + OkHttpClient okHttpClient = new OkHttpClient(); + String keyID = "ABCDEFGHIJKLM"; + String secret = "NOPQURSTUVWXYZ"; + + AlpacaAPI api = new AlpacaAPI(okHttpClient, keyID, secret, null, EndpointAPIType.LIVE, DataAPIType.SIP); + + assertNotNull(api.corporateActions()); + } } diff --git a/src/test/java/net/jacobpeterson/alpaca/test/mock/corporateactions/CorporateActionsEndpointTest.java b/src/test/java/net/jacobpeterson/alpaca/test/mock/corporateactions/CorporateActionsEndpointTest.java new file mode 100644 index 00000000..fd2dc0ae --- /dev/null +++ b/src/test/java/net/jacobpeterson/alpaca/test/mock/corporateactions/CorporateActionsEndpointTest.java @@ -0,0 +1,104 @@ +package net.jacobpeterson.alpaca.test.mock.corporateactions; + +import net.jacobpeterson.alpaca.model.endpoint.corporateactions.common.Announcement; +import net.jacobpeterson.alpaca.rest.AlpacaClient; +import net.jacobpeterson.alpaca.rest.AlpacaClientException; +import net.jacobpeterson.alpaca.rest.endpoint.corporateactions.CorporateActionsEndpoint; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.ResponseBody; +import okhttp3.mock.MockInterceptor; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class CorporateActionsEndpointTest { + + @Test + void getAnnouncements_givenSymbol_shouldReturnData() throws AlpacaClientException { + CorporateActionsEndpoint newsEndpoint = setupEndpoint(mockMultipleAnnouncementsResponse()); + + java.util.List result = newsEndpoint.getAnnouncements("K"); + + assertNotNull(result); + assertEquals(3, result.size()); + + } + + @Test + void getAnnouncements_shouldReturnData() throws AlpacaClientException { + CorporateActionsEndpoint newsEndpoint = setupEndpoint(mockMultipleAnnouncementsResponse()); + + java.util.List result = newsEndpoint.getAnnouncements(); + + assertNotNull(result); + assertEquals(3, result.size()); + + } + + @Test + void getAnnouncements_validation_caTypes() { + CorporateActionsEndpoint newsEndpoint = setupEndpoint(mockMultipleAnnouncementsResponse()); + + assertThrows(Exception.class, () -> newsEndpoint.getAnnouncements(null, LocalDate.now(), LocalDate.now(), null, null, null)); + } + + @Test + void getAnnouncements_validation_since() { + CorporateActionsEndpoint newsEndpoint = setupEndpoint(mockMultipleAnnouncementsResponse()); + + assertThrows(Exception.class, () -> newsEndpoint.getAnnouncements(new ArrayList<>(), null, LocalDate.now(), null, null, null)); + } + + @Test + void getAnnouncements_validation_until() { + CorporateActionsEndpoint newsEndpoint = setupEndpoint(mockMultipleAnnouncementsResponse()); + + assertThrows(Exception.class, () -> newsEndpoint.getAnnouncements(new ArrayList<>(), LocalDate.now(), null, null, null, null)); + } + + @Test + void getAnnouncement_validation_id() { + CorporateActionsEndpoint newsEndpoint = setupEndpoint(mockSingleAnnouncementResponse()); + + assertThrows(Exception.class, () -> newsEndpoint.getAnnouncement(null)); + } + + @Test + void getAnnouncement_shouldReturnData() throws AlpacaClientException { + CorporateActionsEndpoint newsEndpoint = setupEndpoint(mockSingleAnnouncementResponse()); + + Announcement announcement = newsEndpoint.getAnnouncement("1"); + + assertNotNull(announcement); + assertEquals(announcement.getId(), "cd0cb768-889f-47a1-a94b-1438017dad77"); + } + + private static CorporateActionsEndpoint setupEndpoint(String response) { + MockInterceptor interceptor = new MockInterceptor(); + interceptor.addRule() + .respond(200) + .body(ResponseBody.create(response, MediaType.parse("application/json"))); + + OkHttpClient client = new OkHttpClient.Builder() + .addInterceptor(interceptor) + .build(); + + AlpacaClient api = new AlpacaClient(client, "token", "test", "/v1"); + + return new CorporateActionsEndpoint(api); + } + + private static String mockMultipleAnnouncementsResponse() { + return "[{\"id\":\"cd0cb768-889f-47a1-a94b-1438017dad77\",\"corporate_action_id\":\"2642471\",\"ca_type\":\"spinoff\",\"ca_sub_type\":\"spinoff\",\"initiating_symbol\":\"KLG\",\"initiating_original_cusip\":\"92942W107\",\"target_symbol\":\"K\",\"target_original_cusip\":\"487836108\",\"effective_date\":\"2023-09-08\",\"ex_date\":\"2023-10-02\",\"record_date\":\"2023-09-21\",\"payable_date\":\"2023-10-02\",\"cash\":\"0\",\"old_rate\":\"1\",\"new_rate\":\"0.25\"},{\"id\":\"3a7fe233-7766-4f70-915f-9d1a5258eaa9\",\"corporate_action_id\":\"487836108_AD23\",\"ca_type\":\"dividend\",\"ca_sub_type\":\"cash\",\"initiating_symbol\":\"K\",\"initiating_original_cusip\":\"487836108\",\"target_symbol\":\"K\",\"target_original_cusip\":\"487836108\",\"declaration_date\":\"2023-09-19\",\"effective_date\":\"2023-09-21\",\"ex_date\":\"2023-10-02\",\"record_date\":\"2023-09-21\",\"payable_date\":\"2023-10-02\",\"cash\":\"0\",\"old_rate\":\"1\",\"new_rate\":\"1\"},{\"id\":\"ddf64bcb-fa76-4472-a98b-237f220ea38b\",\"corporate_action_id\":\"487836108_AE23\",\"ca_type\":\"dividend\",\"ca_sub_type\":\"cash\",\"initiating_symbol\":\"K\",\"initiating_original_cusip\":\"487836108\",\"target_symbol\":\"K\",\"target_original_cusip\":\"487836108\",\"declaration_date\":\"2023-11-29\",\"effective_date\":\"2023-12-01\",\"ex_date\":\"2023-11-30\",\"record_date\":\"2023-12-01\",\"payable_date\":\"2023-12-15\",\"cash\":\"0.56\",\"old_rate\":\"1\",\"new_rate\":\"1\"}]"; + } + + private static String mockSingleAnnouncementResponse() { + return "{\"id\":\"cd0cb768-889f-47a1-a94b-1438017dad77\",\"corporate_action_id\":\"2642471\",\"ca_type\":\"spinoff\",\"ca_sub_type\":\"spinoff\",\"initiating_symbol\":\"KLG\",\"initiating_original_cusip\":\"92942W107\",\"target_symbol\":\"K\",\"target_original_cusip\":\"487836108\",\"effective_date\":\"2023-09-08\",\"ex_date\":\"2023-10-02\",\"record_date\":\"2023-09-21\",\"payable_date\":\"2023-10-02\",\"cash\":\"0\",\"old_rate\":\"1\",\"new_rate\":\"0.25\"}"; + } +} From 6d5a0436da2de9d6ebadeb5fad76f820a6ea0545 Mon Sep 17 00:00:00 2001 From: gretard Date: Mon, 8 Jan 2024 20:23:20 +0200 Subject: [PATCH 53/84] Add news actions endpoints for #118 --- README.md | 33 +++++ .../control/subscriptions_message.json | 4 + .../enums/market_data_message_type.json | 6 +- .../market_data/news/common/image.json | 13 ++ .../market_data/news/common/news_article.json | 39 ++++++ .../market_data/news/news_response.json | 12 ++ .../news/realtime/news_message.json | 40 ++++++ .../net/jacobpeterson/alpaca/AlpacaAPI.java | 32 ++++- .../marketdata/news/NewsEndpoint.java | 120 ++++++++++++++++++ .../marketdata/MarketDataWebsocket.java | 32 +++-- .../MarketDataWebsocketInterface.java | 11 +- .../news/NewsMarketDataWebsocket.java | 15 +++ .../CorporateActionsEndpointTest.java | 11 +- .../marketdata/news/NewsEndpointTest.java | 111 ++++++++++++++++ 14 files changed, 461 insertions(+), 18 deletions(-) create mode 100644 schema_json/endpoint/market_data/news/common/image.json create mode 100644 schema_json/endpoint/market_data/news/common/news_article.json create mode 100644 schema_json/endpoint/market_data/news/news_response.json create mode 100644 schema_json/endpoint/market_data/news/realtime/news_message.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/news/NewsEndpoint.java create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/news/NewsMarketDataWebsocket.java create mode 100644 src/test/java/net/jacobpeterson/alpaca/test/mock/marketdata/news/NewsEndpointTest.java diff --git a/README.md b/README.md index 82630ee6..c483ee2d 100644 --- a/README.md +++ b/README.md @@ -495,6 +495,39 @@ alpacaAPI.cryptoMarketDataStreaming().subscribe( Arrays.asList("*")); ``` +## [`NewsMarketDataWebsocket`](src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/news/NewsMarketDataWebsocket.java) +Alpaca also offers news websocket streaming for symbols. + +The usage is identical to the [`StockMarketDataWebsocket`](https://github.com/Petersoj/alpaca-java/#stockmarketdatawebsocket) usage except that accessing the websocket instance via the `AlpacaAPI` instance is done using: `alpacaAPI.newsMarketDataStreaming()` instead of `alpacaAPI.stockMarketDataStreaming()`. + +Example usage: +```java +MarketDataListener marketDataListener = (messageType, message) -> + System.out.printf("%s: %s\n", messageType.name(), message); + +MarketDataWebsocketInterface newsWebsocket = alpacaAPI.newsMarketDataStreaming(); + +newsWebsocket.setListener(marketDataListener); +newsWebsocket.subscribeToControl( + MarketDataMessageType.SUCCESS, + MarketDataMessageType.SUBSCRIPTION, + MarketDataMessageType.ERROR); + +newsWebsocket.connect(); +newsWebsocket.waitForAuthorization(5, TimeUnit.SECONDS); +if (!newsWebsocket.isValid()) { + System.out.println("Websocket not valid!"); + return; +} +newsWebsocket.subscribe(null, null, null, Arrays.asList("*")); + +// Wait a few seconds +Thread.sleep(5000); + +// Manually disconnect the websocket +newsWebsocket.disconnect(); +``` + # Building To build this project yourself, clone this repository and run: ``` diff --git a/schema_json/endpoint/market_data/common/realtime/control/subscriptions_message.json b/schema_json/endpoint/market_data/common/realtime/control/subscriptions_message.json index c302083a..b44cbb85 100644 --- a/schema_json/endpoint/market_data/common/realtime/control/subscriptions_message.json +++ b/schema_json/endpoint/market_data/common/realtime/control/subscriptions_message.json @@ -16,6 +16,10 @@ "bars": { "existingJavaType": "java.util.ArrayList", "title": "The {@link java.util.ArrayList} of subscribed bars." + }, + "news": { + "existingJavaType": "java.util.ArrayList", + "title": "The {@link java.util.ArrayList} of subscribed news." } } } diff --git a/schema_json/endpoint/market_data/common/realtime/enums/market_data_message_type.json b/schema_json/endpoint/market_data/common/realtime/enums/market_data_message_type.json index f55a5190..6999050d 100644 --- a/schema_json/endpoint/market_data/common/realtime/enums/market_data_message_type.json +++ b/schema_json/endpoint/market_data/common/realtime/enums/market_data_message_type.json @@ -7,7 +7,8 @@ "subscription", "t", "q", - "b" + "b", + "n" ], "javaEnums": [ { @@ -27,6 +28,9 @@ }, { "name": "BAR" + }, + { + "name": "NEWS" } ] } diff --git a/schema_json/endpoint/market_data/news/common/image.json b/schema_json/endpoint/market_data/news/common/image.json new file mode 100644 index 00000000..2f3bf607 --- /dev/null +++ b/schema_json/endpoint/market_data/news/common/image.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "title": "See News.", + "properties": { + "size": { + "existingJavaType": "java.lang.String" + }, + "url": { + "existingJavaType": "java.lang.String" + } + } + } + \ No newline at end of file diff --git a/schema_json/endpoint/market_data/news/common/news_article.json b/schema_json/endpoint/market_data/news/common/news_article.json new file mode 100644 index 00000000..ee80a6d7 --- /dev/null +++ b/schema_json/endpoint/market_data/news/common/news_article.json @@ -0,0 +1,39 @@ +{ + "type": "object", + "title": "See News.", + "properties": { + "author": { + "existingJavaType": "java.lang.String" + }, + "content": { + "existingJavaType": "java.lang.String" + }, + "created_at": { + "existingJavaType": "java.time.ZonedDateTime" + }, + "headline": { + "existingJavaType": "java.lang.String" + }, + "id": { + "existingJavaType": "java.lang.Long" + }, + "images": { + "existingJavaType": "java.util.ArrayList" + }, + "source": { + "existingJavaType": "java.lang.String" + }, + "summary": { + "existingJavaType": "java.lang.String" + }, + "symbols": { + "existingJavaType": "java.util.ArrayList" + }, + "updated_at": { + "existingJavaType": "java.time.ZonedDateTime" + }, + "url": { + "existingJavaType": "java.lang.String" + } + } + } diff --git a/schema_json/endpoint/market_data/news/news_response.json b/schema_json/endpoint/market_data/news/news_response.json new file mode 100644 index 00000000..b25ed7f9 --- /dev/null +++ b/schema_json/endpoint/market_data/news/news_response.json @@ -0,0 +1,12 @@ +{ + "type": "object", + "title": "See News.", + "properties": { + "news": { + "existingJavaType": "java.util.ArrayList" + }, + "next_page_token": { + "existingJavaType": "java.lang.String" + } + } +} diff --git a/schema_json/endpoint/market_data/news/realtime/news_message.json b/schema_json/endpoint/market_data/news/realtime/news_message.json new file mode 100644 index 00000000..bee011fe --- /dev/null +++ b/schema_json/endpoint/market_data/news/realtime/news_message.json @@ -0,0 +1,40 @@ +{ + "type": "object", + "title": "See News.", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.MarketDataMessage" + }, + "properties": { + "headline": { + "existingJavaType": "java.lang.String" + }, + "created_at": { + "existingJavaType": "java.time.ZonedDateTime" + }, + "updated_at": { + "existingJavaType": "java.time.ZonedDateTime" + }, + "author": { + "existingJavaType": "java.lang.String" + }, + "summary": { + "existingJavaType": "java.lang.String" + }, + "content": { + "existingJavaType": "java.lang.String" + }, + + "url": { + "existingJavaType": "java.lang.String" + }, + "symbols": { + "existingJavaType": "java.util.ArrayList" + }, + "source": { + "existingJavaType": "java.lang.String" + }, + "id": { + "existingJavaType": "java.lang.Long" + } + } + } diff --git a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java index 922ddd10..4292e85d 100644 --- a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java +++ b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java @@ -23,6 +23,8 @@ import net.jacobpeterson.alpaca.websocket.marketdata.MarketDataWebsocketInterface; import net.jacobpeterson.alpaca.websocket.marketdata.crypto.CryptoMarketDataWebsocket; import net.jacobpeterson.alpaca.websocket.marketdata.stock.StockMarketDataWebsocket; +import net.jacobpeterson.alpaca.rest.endpoint.marketdata.news.NewsEndpoint; +import net.jacobpeterson.alpaca.websocket.marketdata.news.NewsMarketDataWebsocket; import net.jacobpeterson.alpaca.websocket.streaming.StreamingWebsocket; import net.jacobpeterson.alpaca.websocket.streaming.StreamingWebsocketInterface; import okhttp3.OkHttpClient; @@ -46,11 +48,13 @@ public class AlpacaAPI { private static final String VERSION_2_PATH_SEGMENT = "v2"; private static final String VERSION_1_BETA_3_PATH_SEGMENT = "v1beta3"; + private static final String VERSION_1_BETA_1_PATH_SEGMENT = "v1beta1"; private final OkHttpClient okHttpClient; private final AlpacaClient brokerClient; private final AlpacaClient cryptoDataClient; private final AlpacaClient stockDataClient; + private final AlpacaClient newsDataClient; // Ordering of fields/methods below are analogous to the ordering in the Alpaca documentation private final AccountEndpoint accountEndpoint; private final CryptoMarketDataEndpoint cryptoMarketDataEndpoint; @@ -67,8 +71,10 @@ public class AlpacaAPI { private final StreamingWebsocket streamingWebsocket; private final CryptoMarketDataWebsocket cryptoMarketDataWebsocket; private final StockMarketDataWebsocket stockMarketDataWebsocket; - private final CorporateActionsEndpoint corporateActionsEndpoint; + private final NewsMarketDataWebsocket newsMarketDataWebsocket; + private final NewsEndpoint newsEndpoint; + /** * Instantiates a new {@link AlpacaAPI} using properties specified in alpaca.properties file (or their * associated defaults). @@ -175,10 +181,12 @@ public AlpacaAPI(OkHttpClient okHttpClient, String keyID, String secretKey, Stri brokerHostSubdomain, VERSION_2_PATH_SEGMENT); cryptoDataClient = new AlpacaClient(okHttpClient, keyID, secretKey, "data", VERSION_1_BETA_3_PATH_SEGMENT); stockDataClient = new AlpacaClient(okHttpClient, keyID, secretKey, "data", VERSION_2_PATH_SEGMENT); + newsDataClient = new AlpacaClient(okHttpClient, keyID, secretKey, "data", VERSION_1_BETA_1_PATH_SEGMENT); } else { brokerClient = new AlpacaClient(okHttpClient, oAuthToken, brokerHostSubdomain, VERSION_2_PATH_SEGMENT); cryptoDataClient = null; stockDataClient = null; + newsDataClient = null; } accountEndpoint = new AccountEndpoint(brokerClient); @@ -200,6 +208,8 @@ public AlpacaAPI(OkHttpClient okHttpClient, String keyID, String secretKey, Stri stockMarketDataWebsocket = stockDataClient == null ? null : new StockMarketDataWebsocket(okHttpClient, dataAPIType, keyID, secretKey); corporateActionsEndpoint = new CorporateActionsEndpoint(brokerClient); + newsEndpoint = newsDataClient == null ? null : new NewsEndpoint(newsDataClient); + newsMarketDataWebsocket = newsDataClient == null ? null : new NewsMarketDataWebsocket(okHttpClient, keyID, secretKey); } /** @@ -323,12 +333,32 @@ public AlpacaClient getStockDataClient() { return stockDataClient; } + public AlpacaClient getNewsDataClient() { + return newsDataClient; + } + /** * @return the {@link CorporateActionsEndpoint} */ public CorporateActionsEndpoint corporateActions() { return corporateActionsEndpoint; } + + /** + * @return the {@link NewsEndpoint} + */ + public NewsEndpoint newsEndpoint() { + return newsEndpoint; + } + + /** + * @return the News {@link MarketDataWebsocketInterface} + */ + public MarketDataWebsocketInterface newsMarketDataStreaming() { + return newsMarketDataWebsocket; + } + + /** * Creates a {@link Builder} for {@link AlpacaAPI}. * diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/news/NewsEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/news/NewsEndpoint.java new file mode 100644 index 00000000..88ee1b7c --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/news/NewsEndpoint.java @@ -0,0 +1,120 @@ +package net.jacobpeterson.alpaca.rest.endpoint.marketdata.news; + +import net.jacobpeterson.alpaca.model.endpoint.common.enums.SortDirection; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.news.NewsResponse; +import net.jacobpeterson.alpaca.rest.AlpacaClient; +import net.jacobpeterson.alpaca.rest.AlpacaClientException; +import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; +import okhttp3.HttpUrl; +import okhttp3.Request; + +import java.time.LocalDate; +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * {@link AlpacaEndpoint} for + * News + * Endpoint. + */ +public class NewsEndpoint extends AlpacaEndpoint { + + /** + * Instantiates a new {@link AlpacaEndpoint}. + * + * @param alpacaClient the {@link AlpacaClient} + */ + public NewsEndpoint(AlpacaClient alpacaClient) { + super(alpacaClient, "news"); + } + + /** + * Gets latest list of news {@link NewsResponse}. + * @return the {@link NewsResponse} + * @throws AlpacaClientException thrown for {@link AlpacaClientException} + */ + public NewsResponse getLatestNews() throws AlpacaClientException { + return this.getLatestNews(null, null, null, null, null, null, null, null); + } + + /** + * Gets latest list of news {@link NewsResponse} for specific symbols. + * @param symbols list of symbols to query news for + * @return the {@link NewsResponse} + * @throws AlpacaClientException thrown for {@link AlpacaClientException}s + */ + public NewsResponse getLatestNews(List symbols) throws AlpacaClientException { + checkNotNull(symbols); + return this.getLatestNews(null, null, null, symbols, null, null, null, null); + } + + /** + * Gets latest list of news {@link NewsResponse} for specified token. + * @param pageToken the ID of the end of your current page of results. (See the section on paging.) + * @return the {@link NewsResponse} + * @throws AlpacaClientException thrown for {@link AlpacaClientException}s + */ + public NewsResponse getLatestNewsPaged(String pageToken) throws AlpacaClientException { + checkNotNull(pageToken); + return this.getLatestNews(null, null, null, null, null, null, null, pageToken); + } + + /** + * Gets latest list of news {@link NewsResponse} + * @param start The inclusive start of the interval + * @param end the inclusive end of the interval + * @param sortDirection sort articles by updated date + * @param symbols list of symbols to query news for + * @param limit Limit of news items to be returned for given page + * @param includeContent Boolean indicator to include content for news articles (if available) + * @param excludeContentless Boolean indicator to exclude news articles that do not contain content + * @param pageToken the ID of the end of your current page of results. (See the section on paging.) + * @return the {@link NewsResponse} + * @throws AlpacaClientException thrown for {@link AlpacaClientException}s + */ + public NewsResponse getLatestNews( + LocalDate start, LocalDate end, + SortDirection sortDirection, + List symbols, + Integer limit, + Boolean includeContent, + Boolean excludeContentless, + String pageToken) throws AlpacaClientException { + + HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() + .addPathSegment(endpointPathSegment); + + if (start != null) { + urlBuilder.addQueryParameter("start", start.toString()); + } + if (end != null) { + urlBuilder.addQueryParameter("end", end.toString()); + } + if (sortDirection != null) { + urlBuilder.addQueryParameter("sort", sortDirection.value()); + } + if (symbols != null && !symbols.isEmpty()) { + urlBuilder.addQueryParameter("symbols", String.join(",", symbols)); + } + if (limit != null) { + urlBuilder.addQueryParameter("limit", Integer.toString(limit)); + } + if (includeContent != null) { + urlBuilder.addQueryParameter("include_content", Boolean.toString(includeContent)); + } + if (excludeContentless != null) { + urlBuilder.addQueryParameter("exclude_contentless", Boolean.toString(excludeContentless)); + } + if (pageToken != null) { + urlBuilder.addQueryParameter("page_token", pageToken); + } + + Request request = alpacaClient.requestBuilder(urlBuilder.build()) + .get() + .build(); + + return alpacaClient.requestObject(request, NewsResponse.class); + + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java index 9c924c11..48c297f9 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java @@ -13,6 +13,8 @@ import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.enums.MarketDataMessageType; import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.quote.QuoteMessage; import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.trade.TradeMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.news.common.NewsArticle; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.news.realtime.NewsMessage; import net.jacobpeterson.alpaca.websocket.AlpacaWebsocket; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; @@ -53,7 +55,8 @@ public abstract class MarketDataWebsocket private static final List SUBSCRIBABLE_MARKET_DATA_MESSAGE_TYPES = Arrays.asList( MarketDataMessageType.TRADE, MarketDataMessageType.QUOTE, - MarketDataMessageType.BAR); + MarketDataMessageType.BAR, + MarketDataMessageType.NEWS); /** * Creates a {@link HttpUrl} for {@link MarketDataWebsocket} with the given websocketURLPathSegments. @@ -74,9 +77,11 @@ private static HttpUrl createWebsocketURL(String websocketURLPathSegments) { private final Set subscribedTrades; private final Set subscribedQuotes; private final Set subscribedBars; + private final Set subscribedNews; private final Type tradeClassType; private final Type quoteClassType; private final Type barClassType; + private final Type newsClassType = NewsMessage.class; /** * Instantiates a new {@link MarketDataWebsocket}. @@ -101,7 +106,7 @@ public MarketDataWebsocket(OkHttpClient okHttpClient, String websocketURLPathSeg subscribedTrades = new HashSet<>(); subscribedQuotes = new HashSet<>(); subscribedBars = new HashSet<>(); - + subscribedNews = new HashSet<>(); this.tradeClassType = tradeClass; this.quoteClassType = quoteClass; this.barClassType = barClass; @@ -124,7 +129,7 @@ protected void onReconnection() { sendAuthenticationMessage(); if (waitForAuthorization(5, TimeUnit.SECONDS)) { subscribeToControl(Iterables.toArray(listenedMarketDataMessageTypes, MarketDataMessageType.class)); - subscribe(subscribedTrades, subscribedQuotes, subscribedBars); + subscribe(subscribedTrades, subscribedQuotes, subscribedBars, subscribedNews); } } @@ -210,6 +215,8 @@ public void onMessage(@NotNull WebSocket webSocket, @NotNull String message) { subscribedQuotes); handleSubscriptionMessageList(MarketDataMessageType.BAR, subscriptionsMessage.getBars(), subscribedBars); + handleSubscriptionMessageList(MarketDataMessageType.NEWS, subscriptionsMessage.getNews(), + subscribedNews); break; case TRADE: marketDataMessage = GSON.fromJson(messageObject, tradeClassType); @@ -220,6 +227,9 @@ public void onMessage(@NotNull WebSocket webSocket, @NotNull String message) { case BAR: marketDataMessage = GSON.fromJson(messageObject, barClassType); break; + case NEWS: + marketDataMessage = GSON.fromJson(messageObject, newsClassType); + break; default: LOGGER.error("Message type {} not implemented!", marketDataMessageType); continue; @@ -286,14 +296,14 @@ public void subscribeToControl(MarketDataMessageType... marketDataMessageTypes) @Override public void subscribe(Collection tradeSymbols, Collection quoteSymbols, - Collection barSymbols) { - sendSubscriptionUpdate(tradeSymbols, quoteSymbols, barSymbols, true); + Collection barSymbols, Collection newsSymbols) { + sendSubscriptionUpdate(tradeSymbols, quoteSymbols, barSymbols, newsSymbols, true); } @Override public void unsubscribe(Collection tradeSymbols, Collection quoteSymbols, - Collection barSymbols) { - sendSubscriptionUpdate(tradeSymbols, quoteSymbols, barSymbols, false); + Collection barSymbols, Collection newsSymbols) { + sendSubscriptionUpdate(tradeSymbols, quoteSymbols, barSymbols, newsSymbols, false); } /** @@ -305,7 +315,7 @@ public void unsubscribe(Collection tradeSymbols, Collection quot * @param subscribe true to subscribe, false to unsubscribe */ private void sendSubscriptionUpdate(Collection tradeSymbols, Collection quoteSymbols, - Collection barSymbols, boolean subscribe) { + Collection barSymbols, Collection newsSymbols, boolean subscribe) { if (!isConnected()) { throw new IllegalStateException("This websocket must be connected before sending subscription updates!"); } @@ -325,6 +335,7 @@ private void sendSubscriptionUpdate(Collection tradeSymbols, Collection< addSubscriptionUpdateList(subscriptionUpdateObject, "trades", tradeSymbols); addSubscriptionUpdateList(subscriptionUpdateObject, "quotes", quoteSymbols); addSubscriptionUpdateList(subscriptionUpdateObject, "bars", barSymbols); + addSubscriptionUpdateList(subscriptionUpdateObject, "news", newsSymbols); boolean updateExists = subscriptionUpdateObject.size() > 1; if (updateExists) { @@ -368,4 +379,9 @@ public Collection subscribedQuotes() { public Collection subscribedBars() { return new HashSet<>(subscribedBars); } + + @Override + public Collection subscribedNews() { + return new HashSet<>(subscribedNews); + } } diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocketInterface.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocketInterface.java index 5ffcaf3c..d4f7700c 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocketInterface.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocketInterface.java @@ -33,9 +33,9 @@ public interface MarketDataWebsocketInterface extends AlpacaWebsocketInterfacenull for no change * @param barSymbols a {@link Collection} of symbols to subscribe to bars or null for no change * - * @see #unsubscribe(Collection, Collection, Collection) + * @see #unsubscribe(Collection, Collection, Collection, Collection) */ - void subscribe(Collection tradeSymbols, Collection quoteSymbols, Collection barSymbols); + void subscribe(Collection tradeSymbols, Collection quoteSymbols, Collection barSymbols, Collection newsSymbols); /** * Unsubscribes from trades, quotes, or bars according to the given {@link Collection} of symbols. @@ -49,9 +49,9 @@ public interface MarketDataWebsocketInterface extends AlpacaWebsocketInterfacenull for no change * - * @see #subscribe(Collection, Collection, Collection) + * @see #subscribe(Collection, Collection, Collection, Collection) */ - void unsubscribe(Collection tradeSymbols, Collection quoteSymbols, Collection barSymbols); + void unsubscribe(Collection tradeSymbols, Collection quoteSymbols, Collection barSymbols, Collection newsSymbols); /** * Gets all the currently subscribed control {@link MarketDataMessageType}s. @@ -80,4 +80,7 @@ public interface MarketDataWebsocketInterface extends AlpacaWebsocketInterface subscribedBars(); + + Collection subscribedNews(); + } diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/news/NewsMarketDataWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/news/NewsMarketDataWebsocket.java new file mode 100644 index 00000000..9d2c2620 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/news/NewsMarketDataWebsocket.java @@ -0,0 +1,15 @@ +package net.jacobpeterson.alpaca.websocket.marketdata.news; + +import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.bar.BarMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.quote.QuoteMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.trade.TradeMessage; +import net.jacobpeterson.alpaca.websocket.marketdata.MarketDataWebsocket; +import okhttp3.OkHttpClient; + +public class NewsMarketDataWebsocket extends MarketDataWebsocket { + public NewsMarketDataWebsocket(OkHttpClient okHttpClient, + String keyID, String secretKey) { + super(okHttpClient, "v1beta1/news", "News", keyID, secretKey, TradeMessage.class, + QuoteMessage.class, BarMessage.class); + } +} diff --git a/src/test/java/net/jacobpeterson/alpaca/test/mock/corporateactions/CorporateActionsEndpointTest.java b/src/test/java/net/jacobpeterson/alpaca/test/mock/corporateactions/CorporateActionsEndpointTest.java index fd2dc0ae..c895ed03 100644 --- a/src/test/java/net/jacobpeterson/alpaca/test/mock/corporateactions/CorporateActionsEndpointTest.java +++ b/src/test/java/net/jacobpeterson/alpaca/test/mock/corporateactions/CorporateActionsEndpointTest.java @@ -45,21 +45,24 @@ void getAnnouncements_shouldReturnData() throws AlpacaClientException { void getAnnouncements_validation_caTypes() { CorporateActionsEndpoint newsEndpoint = setupEndpoint(mockMultipleAnnouncementsResponse()); - assertThrows(Exception.class, () -> newsEndpoint.getAnnouncements(null, LocalDate.now(), LocalDate.now(), null, null, null)); + assertThrows(Exception.class, + () -> newsEndpoint.getAnnouncements(null, LocalDate.now(), LocalDate.now(), null, null, null)); } @Test void getAnnouncements_validation_since() { CorporateActionsEndpoint newsEndpoint = setupEndpoint(mockMultipleAnnouncementsResponse()); - assertThrows(Exception.class, () -> newsEndpoint.getAnnouncements(new ArrayList<>(), null, LocalDate.now(), null, null, null)); + assertThrows(Exception.class, + () -> newsEndpoint.getAnnouncements(new ArrayList<>(), null, LocalDate.now(), null, null, null)); } @Test void getAnnouncements_validation_until() { CorporateActionsEndpoint newsEndpoint = setupEndpoint(mockMultipleAnnouncementsResponse()); - assertThrows(Exception.class, () -> newsEndpoint.getAnnouncements(new ArrayList<>(), LocalDate.now(), null, null, null, null)); + assertThrows(Exception.class, + () -> newsEndpoint.getAnnouncements(new ArrayList<>(), LocalDate.now(), null, null, null, null)); } @Test @@ -76,7 +79,7 @@ void getAnnouncement_shouldReturnData() throws AlpacaClientException { Announcement announcement = newsEndpoint.getAnnouncement("1"); assertNotNull(announcement); - assertEquals(announcement.getId(), "cd0cb768-889f-47a1-a94b-1438017dad77"); + assertEquals("cd0cb768-889f-47a1-a94b-1438017dad77", announcement.getId()); } private static CorporateActionsEndpoint setupEndpoint(String response) { diff --git a/src/test/java/net/jacobpeterson/alpaca/test/mock/marketdata/news/NewsEndpointTest.java b/src/test/java/net/jacobpeterson/alpaca/test/mock/marketdata/news/NewsEndpointTest.java new file mode 100644 index 00000000..ef4b553f --- /dev/null +++ b/src/test/java/net/jacobpeterson/alpaca/test/mock/marketdata/news/NewsEndpointTest.java @@ -0,0 +1,111 @@ +package net.jacobpeterson.alpaca.test.mock.marketdata.news; + +import net.jacobpeterson.alpaca.model.endpoint.marketdata.news.NewsResponse; +import net.jacobpeterson.alpaca.rest.AlpacaClient; +import net.jacobpeterson.alpaca.rest.AlpacaClientException; +import net.jacobpeterson.alpaca.rest.endpoint.marketdata.news.NewsEndpoint; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.ResponseBody; +import okhttp3.mock.MockInterceptor; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class NewsEndpointTest { + + @Test + void getLatestNews_validation_page() { + NewsEndpoint newsEndpoint = setupEndpoint(mockResponse()); + + assertThrows(Exception.class, () -> newsEndpoint.getLatestNewsPaged(null)); + } + + @Test + void getLatestNews_validation_symbols() { + NewsEndpoint newsEndpoint = setupEndpoint(mockResponse()); + + assertThrows(Exception.class, () -> newsEndpoint.getLatestNews(null)); + } + + @Test + void getLatestNews_shouldReturnData() throws AlpacaClientException { + NewsEndpoint newsEndpoint = setupEndpoint(mockResponse()); + NewsResponse response = newsEndpoint.getLatestNews(null, null, null, null, null, null, null, null); + + assertNotNull(response); + assertEquals("MTcwNDczNjg2NTAwMDAwMDAwMHwzNjU0MDM2Mw==", response.getNextPageToken()); + assertEquals(1, response.getNews().size()); + assertEquals(3, response.getNews().get(0).getImages().size()); + + } + + @Test + void getLatestNews_shouldReturnLatestData() throws AlpacaClientException { + NewsEndpoint newsEndpoint = setupEndpoint(mockResponse()); + NewsResponse response = newsEndpoint.getLatestNews(); + + assertNotNull(response); + assertEquals("MTcwNDczNjg2NTAwMDAwMDAwMHwzNjU0MDM2Mw==", response.getNextPageToken()); + assertEquals(1, response.getNews().size()); + assertEquals(3, response.getNews().get(0).getImages().size()); + + } + + private static NewsEndpoint setupEndpoint(String response) { + MockInterceptor interceptor = new MockInterceptor(); + interceptor.addRule() + .respond(200) + .body(ResponseBody.create(response, MediaType.parse("application/json"))); + + OkHttpClient client = new OkHttpClient.Builder() + .addInterceptor(interceptor) + .build(); + + AlpacaClient api = new AlpacaClient(client, "token", "test", "/v1"); + + return new NewsEndpoint(api); + } + + private static String mockResponse() { + return "{\n" + + " \"news\": [\n" + + " {\n" + + " \"author\": \"Khyathi Dalal\",\n" + + " \"content\": \"\",\n" + + " \"created_at\": \"2024-01-08T18:07:34Z\",\n" + + " \"headline\": \"Solana Drops 9.9%, Solana Memecoins Tank Over 20%: Is This The End Of The Dogecoin Killers?\",\n" + + " \"id\": 36536785,\n" + + " \"images\": [\n" + + " {\n" + + " \"size\": \"large\",\n" + + " \"url\": \"https://cdn.benzinga.com/files/imagecache/2048x1536xUP/images/story/2024/01/08/solana_coin_shutter2_0.jpg\"\n" + + " },\n" + + " {\n" + + " \"size\": \"small\",\n" + + " \"url\": \"https://cdn.benzinga.com/files/imagecache/1024x768xUP/images/story/2024/01/08/solana_coin_shutter2_0.jpg\"\n" + + " },\n" + + " {\n" + + " \"size\": \"thumb\",\n" + + " \"url\": \"https://cdn.benzinga.com/files/imagecache/250x187xUP/images/story/2024/01/08/solana_coin_shutter2_0.jpg\"\n" + + " }\n" + + " ],\n" + + " \"source\": \"benzinga\",\n" + + " \"summary\": \"Solana (CRYPTO: SOL) dropped 1.4% in the past 24 hours, extending its losses over the previous seven days to 9.9%. Solana-based memecoins are particularly hard-hit by the correction, taking much heavier losses than established memecoins.\",\n" + + " \"symbols\": [\n" + + " \"BONKUSD\",\n" + + " \"DOGEUSD\",\n" + + " \"ETHUSD\",\n" + + " \"SHIBUSD\",\n" + + " \"SOLUSD\"\n" + + " ],\n" + + " \"updated_at\": \"2024-01-08T18:07:35Z\",\n" + + " \"url\": \"https://www.benzinga.com/markets/cryptocurrency/24/01/36536785/solana-drops-9-9-solana-memecoins-tank-over-20-is-this-the-end-of-the-dogecoin-killers\"\n" + + " }\n" + + " ],\n" + + " \"next_page_token\": \"MTcwNDczNjg2NTAwMDAwMDAwMHwzNjU0MDM2Mw==\"\n" + + "}"; + } +} From 2750936a15114a750ce604e741fb8c6e88d87052 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Fri, 1 Mar 2024 17:16:16 -0800 Subject: [PATCH 54/84] Modify PR to account for future project structure changes Namely: moving to OpenAPI generated clients, removing unit testing for RestAPI --- build.gradle | 1 - .../alpaca/test/mock/AlpacaAPITest.java | 13 -- .../CorporateActionsEndpointTest.java | 107 ----------------- .../marketdata/news/NewsEndpointTest.java | 111 ------------------ 4 files changed, 232 deletions(-) delete mode 100644 src/test/java/net/jacobpeterson/alpaca/test/mock/corporateactions/CorporateActionsEndpointTest.java delete mode 100644 src/test/java/net/jacobpeterson/alpaca/test/mock/marketdata/news/NewsEndpointTest.java diff --git a/build.gradle b/build.gradle index 7c65cc3d..a861d055 100644 --- a/build.gradle +++ b/build.gradle @@ -53,7 +53,6 @@ dependencies { testImplementation group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.36' testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter', version: '5.8.2' testImplementation group: 'org.mockito', name: 'mockito-core', version: '4.6.1' - testImplementation group: 'com.github.gmazzo.okhttp.mock', name: 'mock-client', version: '2.0.0' } // Exclude any SLF4j-implementation transitive dependencies so users can use a logging framework of their choice diff --git a/src/test/java/net/jacobpeterson/alpaca/test/mock/AlpacaAPITest.java b/src/test/java/net/jacobpeterson/alpaca/test/mock/AlpacaAPITest.java index 0fbc722a..d5f4f320 100644 --- a/src/test/java/net/jacobpeterson/alpaca/test/mock/AlpacaAPITest.java +++ b/src/test/java/net/jacobpeterson/alpaca/test/mock/AlpacaAPITest.java @@ -6,8 +6,6 @@ import okhttp3.OkHttpClient; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertNotNull; - /** * {@link AlpacaAPITest} tests {@link AlpacaAPI} using mocked objects with Mockito. */ @@ -73,15 +71,4 @@ public void testAlpacaAPIConstructor_okHttpClient_keyID_secret_oAuthToken_endpoi new AlpacaAPI(okHttpClient, null, null, oAuthToken, EndpointAPIType.PAPER, DataAPIType.IEX); new AlpacaAPI(okHttpClient, keyID, secret, null, EndpointAPIType.LIVE, DataAPIType.SIP); } - - @Test - public void testAlpacaAPIConstructor_corporateActionsEndpoint() { - OkHttpClient okHttpClient = new OkHttpClient(); - String keyID = "ABCDEFGHIJKLM"; - String secret = "NOPQURSTUVWXYZ"; - - AlpacaAPI api = new AlpacaAPI(okHttpClient, keyID, secret, null, EndpointAPIType.LIVE, DataAPIType.SIP); - - assertNotNull(api.corporateActions()); - } } diff --git a/src/test/java/net/jacobpeterson/alpaca/test/mock/corporateactions/CorporateActionsEndpointTest.java b/src/test/java/net/jacobpeterson/alpaca/test/mock/corporateactions/CorporateActionsEndpointTest.java deleted file mode 100644 index c895ed03..00000000 --- a/src/test/java/net/jacobpeterson/alpaca/test/mock/corporateactions/CorporateActionsEndpointTest.java +++ /dev/null @@ -1,107 +0,0 @@ -package net.jacobpeterson.alpaca.test.mock.corporateactions; - -import net.jacobpeterson.alpaca.model.endpoint.corporateactions.common.Announcement; -import net.jacobpeterson.alpaca.rest.AlpacaClient; -import net.jacobpeterson.alpaca.rest.AlpacaClientException; -import net.jacobpeterson.alpaca.rest.endpoint.corporateactions.CorporateActionsEndpoint; -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.ResponseBody; -import okhttp3.mock.MockInterceptor; -import org.junit.jupiter.api.Test; - -import java.time.LocalDate; -import java.util.ArrayList; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class CorporateActionsEndpointTest { - - @Test - void getAnnouncements_givenSymbol_shouldReturnData() throws AlpacaClientException { - CorporateActionsEndpoint newsEndpoint = setupEndpoint(mockMultipleAnnouncementsResponse()); - - java.util.List result = newsEndpoint.getAnnouncements("K"); - - assertNotNull(result); - assertEquals(3, result.size()); - - } - - @Test - void getAnnouncements_shouldReturnData() throws AlpacaClientException { - CorporateActionsEndpoint newsEndpoint = setupEndpoint(mockMultipleAnnouncementsResponse()); - - java.util.List result = newsEndpoint.getAnnouncements(); - - assertNotNull(result); - assertEquals(3, result.size()); - - } - - @Test - void getAnnouncements_validation_caTypes() { - CorporateActionsEndpoint newsEndpoint = setupEndpoint(mockMultipleAnnouncementsResponse()); - - assertThrows(Exception.class, - () -> newsEndpoint.getAnnouncements(null, LocalDate.now(), LocalDate.now(), null, null, null)); - } - - @Test - void getAnnouncements_validation_since() { - CorporateActionsEndpoint newsEndpoint = setupEndpoint(mockMultipleAnnouncementsResponse()); - - assertThrows(Exception.class, - () -> newsEndpoint.getAnnouncements(new ArrayList<>(), null, LocalDate.now(), null, null, null)); - } - - @Test - void getAnnouncements_validation_until() { - CorporateActionsEndpoint newsEndpoint = setupEndpoint(mockMultipleAnnouncementsResponse()); - - assertThrows(Exception.class, - () -> newsEndpoint.getAnnouncements(new ArrayList<>(), LocalDate.now(), null, null, null, null)); - } - - @Test - void getAnnouncement_validation_id() { - CorporateActionsEndpoint newsEndpoint = setupEndpoint(mockSingleAnnouncementResponse()); - - assertThrows(Exception.class, () -> newsEndpoint.getAnnouncement(null)); - } - - @Test - void getAnnouncement_shouldReturnData() throws AlpacaClientException { - CorporateActionsEndpoint newsEndpoint = setupEndpoint(mockSingleAnnouncementResponse()); - - Announcement announcement = newsEndpoint.getAnnouncement("1"); - - assertNotNull(announcement); - assertEquals("cd0cb768-889f-47a1-a94b-1438017dad77", announcement.getId()); - } - - private static CorporateActionsEndpoint setupEndpoint(String response) { - MockInterceptor interceptor = new MockInterceptor(); - interceptor.addRule() - .respond(200) - .body(ResponseBody.create(response, MediaType.parse("application/json"))); - - OkHttpClient client = new OkHttpClient.Builder() - .addInterceptor(interceptor) - .build(); - - AlpacaClient api = new AlpacaClient(client, "token", "test", "/v1"); - - return new CorporateActionsEndpoint(api); - } - - private static String mockMultipleAnnouncementsResponse() { - return "[{\"id\":\"cd0cb768-889f-47a1-a94b-1438017dad77\",\"corporate_action_id\":\"2642471\",\"ca_type\":\"spinoff\",\"ca_sub_type\":\"spinoff\",\"initiating_symbol\":\"KLG\",\"initiating_original_cusip\":\"92942W107\",\"target_symbol\":\"K\",\"target_original_cusip\":\"487836108\",\"effective_date\":\"2023-09-08\",\"ex_date\":\"2023-10-02\",\"record_date\":\"2023-09-21\",\"payable_date\":\"2023-10-02\",\"cash\":\"0\",\"old_rate\":\"1\",\"new_rate\":\"0.25\"},{\"id\":\"3a7fe233-7766-4f70-915f-9d1a5258eaa9\",\"corporate_action_id\":\"487836108_AD23\",\"ca_type\":\"dividend\",\"ca_sub_type\":\"cash\",\"initiating_symbol\":\"K\",\"initiating_original_cusip\":\"487836108\",\"target_symbol\":\"K\",\"target_original_cusip\":\"487836108\",\"declaration_date\":\"2023-09-19\",\"effective_date\":\"2023-09-21\",\"ex_date\":\"2023-10-02\",\"record_date\":\"2023-09-21\",\"payable_date\":\"2023-10-02\",\"cash\":\"0\",\"old_rate\":\"1\",\"new_rate\":\"1\"},{\"id\":\"ddf64bcb-fa76-4472-a98b-237f220ea38b\",\"corporate_action_id\":\"487836108_AE23\",\"ca_type\":\"dividend\",\"ca_sub_type\":\"cash\",\"initiating_symbol\":\"K\",\"initiating_original_cusip\":\"487836108\",\"target_symbol\":\"K\",\"target_original_cusip\":\"487836108\",\"declaration_date\":\"2023-11-29\",\"effective_date\":\"2023-12-01\",\"ex_date\":\"2023-11-30\",\"record_date\":\"2023-12-01\",\"payable_date\":\"2023-12-15\",\"cash\":\"0.56\",\"old_rate\":\"1\",\"new_rate\":\"1\"}]"; - } - - private static String mockSingleAnnouncementResponse() { - return "{\"id\":\"cd0cb768-889f-47a1-a94b-1438017dad77\",\"corporate_action_id\":\"2642471\",\"ca_type\":\"spinoff\",\"ca_sub_type\":\"spinoff\",\"initiating_symbol\":\"KLG\",\"initiating_original_cusip\":\"92942W107\",\"target_symbol\":\"K\",\"target_original_cusip\":\"487836108\",\"effective_date\":\"2023-09-08\",\"ex_date\":\"2023-10-02\",\"record_date\":\"2023-09-21\",\"payable_date\":\"2023-10-02\",\"cash\":\"0\",\"old_rate\":\"1\",\"new_rate\":\"0.25\"}"; - } -} diff --git a/src/test/java/net/jacobpeterson/alpaca/test/mock/marketdata/news/NewsEndpointTest.java b/src/test/java/net/jacobpeterson/alpaca/test/mock/marketdata/news/NewsEndpointTest.java deleted file mode 100644 index ef4b553f..00000000 --- a/src/test/java/net/jacobpeterson/alpaca/test/mock/marketdata/news/NewsEndpointTest.java +++ /dev/null @@ -1,111 +0,0 @@ -package net.jacobpeterson.alpaca.test.mock.marketdata.news; - -import net.jacobpeterson.alpaca.model.endpoint.marketdata.news.NewsResponse; -import net.jacobpeterson.alpaca.rest.AlpacaClient; -import net.jacobpeterson.alpaca.rest.AlpacaClientException; -import net.jacobpeterson.alpaca.rest.endpoint.marketdata.news.NewsEndpoint; -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.ResponseBody; -import okhttp3.mock.MockInterceptor; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -class NewsEndpointTest { - - @Test - void getLatestNews_validation_page() { - NewsEndpoint newsEndpoint = setupEndpoint(mockResponse()); - - assertThrows(Exception.class, () -> newsEndpoint.getLatestNewsPaged(null)); - } - - @Test - void getLatestNews_validation_symbols() { - NewsEndpoint newsEndpoint = setupEndpoint(mockResponse()); - - assertThrows(Exception.class, () -> newsEndpoint.getLatestNews(null)); - } - - @Test - void getLatestNews_shouldReturnData() throws AlpacaClientException { - NewsEndpoint newsEndpoint = setupEndpoint(mockResponse()); - NewsResponse response = newsEndpoint.getLatestNews(null, null, null, null, null, null, null, null); - - assertNotNull(response); - assertEquals("MTcwNDczNjg2NTAwMDAwMDAwMHwzNjU0MDM2Mw==", response.getNextPageToken()); - assertEquals(1, response.getNews().size()); - assertEquals(3, response.getNews().get(0).getImages().size()); - - } - - @Test - void getLatestNews_shouldReturnLatestData() throws AlpacaClientException { - NewsEndpoint newsEndpoint = setupEndpoint(mockResponse()); - NewsResponse response = newsEndpoint.getLatestNews(); - - assertNotNull(response); - assertEquals("MTcwNDczNjg2NTAwMDAwMDAwMHwzNjU0MDM2Mw==", response.getNextPageToken()); - assertEquals(1, response.getNews().size()); - assertEquals(3, response.getNews().get(0).getImages().size()); - - } - - private static NewsEndpoint setupEndpoint(String response) { - MockInterceptor interceptor = new MockInterceptor(); - interceptor.addRule() - .respond(200) - .body(ResponseBody.create(response, MediaType.parse("application/json"))); - - OkHttpClient client = new OkHttpClient.Builder() - .addInterceptor(interceptor) - .build(); - - AlpacaClient api = new AlpacaClient(client, "token", "test", "/v1"); - - return new NewsEndpoint(api); - } - - private static String mockResponse() { - return "{\n" + - " \"news\": [\n" + - " {\n" + - " \"author\": \"Khyathi Dalal\",\n" + - " \"content\": \"\",\n" + - " \"created_at\": \"2024-01-08T18:07:34Z\",\n" + - " \"headline\": \"Solana Drops 9.9%, Solana Memecoins Tank Over 20%: Is This The End Of The Dogecoin Killers?\",\n" + - " \"id\": 36536785,\n" + - " \"images\": [\n" + - " {\n" + - " \"size\": \"large\",\n" + - " \"url\": \"https://cdn.benzinga.com/files/imagecache/2048x1536xUP/images/story/2024/01/08/solana_coin_shutter2_0.jpg\"\n" + - " },\n" + - " {\n" + - " \"size\": \"small\",\n" + - " \"url\": \"https://cdn.benzinga.com/files/imagecache/1024x768xUP/images/story/2024/01/08/solana_coin_shutter2_0.jpg\"\n" + - " },\n" + - " {\n" + - " \"size\": \"thumb\",\n" + - " \"url\": \"https://cdn.benzinga.com/files/imagecache/250x187xUP/images/story/2024/01/08/solana_coin_shutter2_0.jpg\"\n" + - " }\n" + - " ],\n" + - " \"source\": \"benzinga\",\n" + - " \"summary\": \"Solana (CRYPTO: SOL) dropped 1.4% in the past 24 hours, extending its losses over the previous seven days to 9.9%. Solana-based memecoins are particularly hard-hit by the correction, taking much heavier losses than established memecoins.\",\n" + - " \"symbols\": [\n" + - " \"BONKUSD\",\n" + - " \"DOGEUSD\",\n" + - " \"ETHUSD\",\n" + - " \"SHIBUSD\",\n" + - " \"SOLUSD\"\n" + - " ],\n" + - " \"updated_at\": \"2024-01-08T18:07:35Z\",\n" + - " \"url\": \"https://www.benzinga.com/markets/cryptocurrency/24/01/36536785/solana-drops-9-9-solana-memecoins-tank-over-20-is-this-the-end-of-the-dogecoin-killers\"\n" + - " }\n" + - " ],\n" + - " \"next_page_token\": \"MTcwNDczNjg2NTAwMDAwMDAwMHwzNjU0MDM2Mw==\"\n" + - "}"; - } -} From 1f6e425087ea963e0497f616bd8f411b09d3f9b5 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Fri, 1 Mar 2024 20:33:54 -0800 Subject: [PATCH 55/84] WIP use OpenAPI specification with generated clients --- .editorconfig | 10 +- .gitattributes | 45 +- .github/image/logo.png | Bin 0 -> 52737 bytes .gitignore | 3 +- .travis.yml | 24 - LICENSE => LICENSE.txt | 2 +- README.md | 47 +- _config.yml | 2 +- build.gradle | 376 +++++----- buildSrc/build.gradle | 29 + .../GeneratePOJOsTask.groovy | 128 ++++ .../JSONSchema2POJOAdaptedPlugin.groovy | 26 + .../JSONSchemaFileConfig.groovy | 31 + codecov.yml | 6 - gradle/wrapper/.gitignore | 1 + gradle/wrapper/gradle-wrapper.properties | 4 +- schema_json/endpoint/account/account.json | 110 --- .../account/enums/account_status.json | 44 -- .../account_activities/account_activity.json | 14 - .../enums/activity_type.json | 152 ---- .../enums/trade_activity_side.json | 8 - .../enums/trade_activity_type.json | 8 - .../non_trade_activity.json | 38 - .../account_activities/trade_activity.json | 48 -- .../account_configuration.json | 22 - .../enums/dtbp_check.json | 10 - .../enums/trade_confirm_email.json | 8 - schema_json/endpoint/assets/asset.json | 51 -- .../endpoint/assets/enums/asset_class.json | 8 - .../endpoint/assets/enums/asset_status.json | 8 - schema_json/endpoint/calendar/calendar.json | 18 - schema_json/endpoint/clock/clock.json | 22 - .../endpoint/common/enums/sort_direction.json | 16 - .../common/announcement.json | 53 -- .../endpoint/orders/cancelled_order.json | 19 - .../orders/enums/current_order_status.json | 9 - .../endpoint/orders/enums/order_class.json | 24 - .../endpoint/orders/enums/order_side.json | 8 - .../endpoint/orders/enums/order_status.json | 76 -- .../orders/enums/order_time_in_force.json | 38 - .../endpoint/orders/enums/order_type.json | 28 - schema_json/endpoint/orders/order.json | 134 ---- .../enums/portfolio_period_unit.json | 24 - .../enums/portfolio_time_frame.json | 28 - .../portfolio_history/portfolio_history.json | 18 - .../portfolio_history_data_point.json | 22 - .../portfolio_history_response.json | 31 - .../positions/close_position_order.json | 19 - schema_json/endpoint/positions/position.json | 81 --- schema_json/endpoint/watchlist/watchlist.json | 30 - .../net/jacobpeterson/alpaca/AlpacaAPI.java | 246 +------ .../common/historical/bar/bar.json | 0 .../historical/bar/enums/bar_time_period.json | 0 .../common/historical/quote/quote.json | 0 .../common/historical/trade/trade.json | 0 .../common/realtime/bar/bar_message.json | 0 .../realtime/control/error_message.json | 0 .../control/subscriptions_message.json | 0 .../realtime/control/success_message.json | 0 .../enums/market_data_message_type.json | 0 .../common/realtime/market_data_message.json | 0 .../common/realtime/quote/quote_message.json | 0 .../common/realtime/symbol_message.json | 0 .../common/realtime/trade/trade_message.json | 0 .../crypto/common/enums/taker_side.json | 0 .../crypto/historical/bar/crypto_bar.json | 0 .../historical/bar/crypto_bars_response.json | 0 .../bar/latest_crypto_bars_response.json | 0 .../orderbook/crypto_orderbook.json | 0 .../orderbook/crypto_orderbook_entry.json | 0 .../latest_crypto_orderbooks_response.json | 0 .../crypto/historical/quote/crypto_quote.json | 0 .../quote/latest_crypto_quotes_response.json | 0 .../historical/snapshot/crypto_snapshot.json | 0 .../snapshot/crypto_snapshots_response.json | 0 .../crypto/historical/trade/crypto_trade.json | 0 .../trade/crypto_trades_response.json | 0 .../trade/latest_crypto_trades_response.json | 0 .../realtime/bar/crypto_bar_message.json | 0 .../realtime/quote/crypto_quote_message.json | 0 .../realtime/trade/crypto_trade_message.json | 0 .../market_data/news/common/image.json | 0 .../market_data/news/common/news_article.json | 0 .../market_data/news/news_response.json | 0 .../news/realtime/news_message.json | 0 .../historical/bar/enums/bar_adjustment.json | 0 .../stock/historical/bar/enums/bar_feed.json | 0 .../bar/multi_stock_bars_response.json | 0 .../stock/historical/bar/stock_bar.json | 0 .../historical/bar/stock_bars_response.json | 0 .../quote/latest_stock_quote_response.json | 0 .../stock/historical/quote/stock_quote.json | 0 .../quote/stock_quotes_response.json | 0 .../stock/historical/snapshot/snapshot.json | 0 .../trade/latest_stock_trade_response.json | 0 .../stock/historical/trade/stock_trade.json | 0 .../trade/stock_trades_response.json | 0 .../stock/realtime/bar/stock_bar_message.json | 0 .../realtime/quote/stock_quote_message.json | 0 .../realtime/trade/stock_trade_message.json | 0 .../authorization/authorization_data.json | 0 .../authorization/authorization_message.json | 0 .../enums/streaming_message_type.json | 0 .../streaming/listening/listening_data.json | 0 .../listening/listening_message.json | 0 .../endpoint/streaming/streaming_message.json | 0 .../trade/enums/trade_update_event.json | 0 .../streaming/trade/trade_update.json | 4 +- .../streaming/trade/trade_update_message.json | 0 .../alpaca/properties/AlpacaProperties.java | 35 +- .../alpaca}/properties/data_api_type.json | 0 .../alpaca}/properties/endpoint_api_type.json | 0 .../alpaca/rest/AlpacaClient.java | 235 ------- .../alpaca/rest/AlpacaClientException.java | 144 ---- .../alpaca/rest/endpoint/AlpacaEndpoint.java | 23 - .../endpoint/account/AccountEndpoint.java | 39 -- .../AccountActivitiesEndpoint.java | 142 ---- .../AccountConfigurationEndpoint.java | 64 -- .../rest/endpoint/assets/AssetsEndpoint.java | 83 --- .../endpoint/calendar/CalendarEndpoint.java | 71 -- .../rest/endpoint/clock/ClockEndpoint.java | 39 -- .../CorporateActionsEndpoint.java | 121 ---- .../crypto/CryptoMarketDataEndpoint.java | 302 -------- .../marketdata/news/NewsEndpoint.java | 120 ---- .../stock/StockMarketDataEndpoint.java | 368 ---------- .../rest/endpoint/orders/OrdersEndpoint.java | 651 ------------------ .../PortfolioHistoryEndpoint.java | 103 --- .../endpoint/positions/PositionsEndpoint.java | 136 ---- .../endpoint/watchlist/WatchlistEndpoint.java | 216 ------ .../util/doc/Documentation2JSONSchema.java | 130 ---- .../alpaca/util/format/FormatUtil.java | 53 -- .../alpaca/util/okhttp/JSONBodyBuilder.java | 65 -- .../alpaca/util/properties/PropertyUtil.java | 88 ++- .../alpaca/websocket/AlpacaWebsocket.java | 3 +- .../WebsocketStateListener.java | 2 +- .../streaming/StreamingWebsocket.java | 18 +- src/main/resources/alpaca.default.properties | 12 +- .../alpaca/test/live/AlpacaAPITest.java | 289 -------- .../alpaca/test/mock/AlpacaAPITest.java | 74 -- src/test/resources/alpaca.default.properties | 5 - 140 files changed, 528 insertions(+), 5314 deletions(-) create mode 100644 .github/image/logo.png delete mode 100644 .travis.yml rename LICENSE => LICENSE.txt (96%) create mode 100644 buildSrc/build.gradle create mode 100644 buildSrc/src/main/groovy/net/jacobpeterson/alpaca/buildsrc/plugin/jsonschema2pojoadapted/GeneratePOJOsTask.groovy create mode 100644 buildSrc/src/main/groovy/net/jacobpeterson/alpaca/buildsrc/plugin/jsonschema2pojoadapted/JSONSchema2POJOAdaptedPlugin.groovy create mode 100644 buildSrc/src/main/groovy/net/jacobpeterson/alpaca/buildsrc/plugin/jsonschema2pojoadapted/JSONSchemaFileConfig.groovy delete mode 100644 codecov.yml create mode 100644 gradle/wrapper/.gitignore delete mode 100644 schema_json/endpoint/account/account.json delete mode 100644 schema_json/endpoint/account/enums/account_status.json delete mode 100644 schema_json/endpoint/account_activities/account_activity.json delete mode 100644 schema_json/endpoint/account_activities/enums/activity_type.json delete mode 100644 schema_json/endpoint/account_activities/enums/trade_activity_side.json delete mode 100644 schema_json/endpoint/account_activities/enums/trade_activity_type.json delete mode 100644 schema_json/endpoint/account_activities/non_trade_activity.json delete mode 100644 schema_json/endpoint/account_activities/trade_activity.json delete mode 100644 schema_json/endpoint/account_configuration/account_configuration.json delete mode 100644 schema_json/endpoint/account_configuration/enums/dtbp_check.json delete mode 100644 schema_json/endpoint/account_configuration/enums/trade_confirm_email.json delete mode 100644 schema_json/endpoint/assets/asset.json delete mode 100644 schema_json/endpoint/assets/enums/asset_class.json delete mode 100644 schema_json/endpoint/assets/enums/asset_status.json delete mode 100644 schema_json/endpoint/calendar/calendar.json delete mode 100644 schema_json/endpoint/clock/clock.json delete mode 100644 schema_json/endpoint/common/enums/sort_direction.json delete mode 100644 schema_json/endpoint/corporate_actions/common/announcement.json delete mode 100644 schema_json/endpoint/orders/cancelled_order.json delete mode 100644 schema_json/endpoint/orders/enums/current_order_status.json delete mode 100644 schema_json/endpoint/orders/enums/order_class.json delete mode 100644 schema_json/endpoint/orders/enums/order_side.json delete mode 100644 schema_json/endpoint/orders/enums/order_status.json delete mode 100644 schema_json/endpoint/orders/enums/order_time_in_force.json delete mode 100644 schema_json/endpoint/orders/enums/order_type.json delete mode 100644 schema_json/endpoint/orders/order.json delete mode 100644 schema_json/endpoint/portfolio_history/enums/portfolio_period_unit.json delete mode 100644 schema_json/endpoint/portfolio_history/enums/portfolio_time_frame.json delete mode 100644 schema_json/endpoint/portfolio_history/portfolio_history.json delete mode 100644 schema_json/endpoint/portfolio_history/portfolio_history_data_point.json delete mode 100644 schema_json/endpoint/portfolio_history/portfolio_history_response.json delete mode 100644 schema_json/endpoint/positions/close_position_order.json delete mode 100644 schema_json/endpoint/positions/position.json delete mode 100644 schema_json/endpoint/watchlist/watchlist.json rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/common/historical/bar/bar.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/common/historical/bar/enums/bar_time_period.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/common/historical/quote/quote.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/common/historical/trade/trade.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/common/realtime/bar/bar_message.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/common/realtime/control/error_message.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/common/realtime/control/subscriptions_message.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/common/realtime/control/success_message.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/common/realtime/enums/market_data_message_type.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/common/realtime/market_data_message.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/common/realtime/quote/quote_message.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/common/realtime/symbol_message.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/common/realtime/trade/trade_message.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/crypto/common/enums/taker_side.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/crypto/historical/bar/crypto_bar.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/crypto/historical/bar/crypto_bars_response.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/crypto/historical/bar/latest_crypto_bars_response.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/crypto/historical/orderbook/crypto_orderbook.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/crypto/historical/orderbook/crypto_orderbook_entry.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/crypto/historical/orderbook/latest_crypto_orderbooks_response.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/crypto/historical/quote/crypto_quote.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/crypto/historical/quote/latest_crypto_quotes_response.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/crypto/historical/snapshot/crypto_snapshot.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/crypto/historical/snapshot/crypto_snapshots_response.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/crypto/historical/trade/crypto_trade.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/crypto/historical/trade/crypto_trades_response.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/crypto/historical/trade/latest_crypto_trades_response.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/crypto/realtime/bar/crypto_bar_message.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/crypto/realtime/quote/crypto_quote_message.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/crypto/realtime/trade/crypto_trade_message.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/news/common/image.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/news/common/news_article.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/news/news_response.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/news/realtime/news_message.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/stock/historical/bar/enums/bar_adjustment.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/stock/historical/bar/enums/bar_feed.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/stock/historical/bar/multi_stock_bars_response.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/stock/historical/bar/stock_bar.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/stock/historical/bar/stock_bars_response.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/stock/historical/quote/latest_stock_quote_response.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/stock/historical/quote/stock_quote.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/stock/historical/quote/stock_quotes_response.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/stock/historical/snapshot/snapshot.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/stock/historical/trade/latest_stock_trade_response.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/stock/historical/trade/stock_trade.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/stock/historical/trade/stock_trades_response.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/stock/realtime/bar/stock_bar_message.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/stock/realtime/quote/stock_quote_message.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/market_data/stock/realtime/trade/stock_trade_message.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/streaming/authorization/authorization_data.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/streaming/authorization/authorization_message.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/streaming/enums/streaming_message_type.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/streaming/listening/listening_data.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/streaming/listening/listening_message.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/streaming/streaming_message.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/streaming/trade/enums/trade_update_event.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/streaming/trade/trade_update.json (82%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/endpoint/streaming/trade/trade_update_message.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/properties/data_api_type.json (100%) rename {schema_json => src/main/java/net/jacobpeterson/alpaca}/properties/endpoint_api_type.json (100%) delete mode 100644 src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClient.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClientException.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AlpacaEndpoint.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/rest/endpoint/account/AccountEndpoint.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/rest/endpoint/accountactivities/AccountActivitiesEndpoint.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/rest/endpoint/accountconfiguration/AccountConfigurationEndpoint.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/rest/endpoint/assets/AssetsEndpoint.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/rest/endpoint/calendar/CalendarEndpoint.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/rest/endpoint/clock/ClockEndpoint.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/rest/endpoint/corporateactions/CorporateActionsEndpoint.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/crypto/CryptoMarketDataEndpoint.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/news/NewsEndpoint.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/stock/StockMarketDataEndpoint.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/rest/endpoint/orders/OrdersEndpoint.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/rest/endpoint/portfoliohistory/PortfolioHistoryEndpoint.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/rest/endpoint/positions/PositionsEndpoint.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/rest/endpoint/watchlist/WatchlistEndpoint.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/util/doc/Documentation2JSONSchema.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/util/format/FormatUtil.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/util/okhttp/JSONBodyBuilder.java rename src/main/java/net/jacobpeterson/alpaca/{util/okhttp => websocket}/WebsocketStateListener.java (94%) delete mode 100644 src/test/java/net/jacobpeterson/alpaca/test/live/AlpacaAPITest.java delete mode 100644 src/test/java/net/jacobpeterson/alpaca/test/mock/AlpacaAPITest.java delete mode 100644 src/test/resources/alpaca.default.properties diff --git a/.editorconfig b/.editorconfig index 818a4c9e..e8ed66b1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -509,7 +509,7 @@ ij_typescript_finally_on_new_line = false ij_typescript_for_brace_force = always ij_typescript_for_statement_new_line_after_left_paren = false ij_typescript_for_statement_right_paren_on_new_line = false -ij_typescript_for_statement_wrap = split_into_lines +ij_typescript_for_statement_wrap = normal ij_typescript_force_quote_style = true ij_typescript_force_semicolon_style = true ij_typescript_function_expression_brace_style = end_of_line @@ -519,7 +519,7 @@ ij_typescript_import_prefer_absolute_path = global ij_typescript_import_sort_members = true ij_typescript_import_sort_module_name = false ij_typescript_import_use_node_resolution = true -ij_typescript_imports_wrap = normal +ij_typescript_imports_wrap = on_every_item ij_typescript_indent_case_from_switch = true ij_typescript_indent_chained_calls = true ij_typescript_indent_package_children = 0 @@ -1023,7 +1023,7 @@ ij_json_wrap_long_lines = false [{*.htm,*.html,*.ng,*.peb,*.sht,*.shtm,*.shtml}] ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3 -ij_html_align_attributes = true +ij_html_align_attributes = false ij_html_align_text = false ij_html_attribute_wrap = normal ij_html_block_comment_add_space = false @@ -1182,8 +1182,8 @@ ij_prototext_spaces_within_brackets = false [{*.properties,spring.handlers,spring.schemas}] ij_properties_align_group_field_declarations = false -ij_properties_keep_blank_lines = false -ij_properties_key_value_delimiter = equals +ij_properties_keep_blank_lines = true +ij_properties_key_value_delimiter = colon ij_properties_spaces_around_key_value_delimiter = false [{*.qute.htm,*.qute.html,*.qute.json,*.qute.txt,*.qute.yaml,*.qute.yml}] diff --git a/.gitattributes b/.gitattributes index 1730d965..710d818f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,46 +1,7 @@ -# https://www.gitattributes.io/api/java -# -# Handle line endings automatically for files detected as text -# and leave all files detected as binary untouched. +# Handle line endings automatically for files detected as text and leave all files detected as binary untouched. * text=auto -# -# The above will handle all files NOT found below -# -# These files are text and should be normalized (Convert crlf => lf) -*.css text -*.df text -*.htm text -*.html text -*.java text -*.js text -*.json text -*.jsp text -*.jspf text -*.jspx text -*.properties text -*.sh text -*.tld text -*.txt text -*.tag text -*.tagx text -*.xml text -*.yml text - -# Declare files that will always have CRLF line endings on checkout. +# Declare files that should always have CRLF line endings at checkout. *.sln text eol=crlf *.bat text eol=crlf - -# These files are binary and should be left untouched -# (binary is a macro for -text -diff) -*.class binary -*.dll binary -*.ear binary -*.gif binary -*.ico binary -*.jar binary -*.jpg binary -*.jpeg binary -*.png binary -*.so binary -*.war binary +*.cmd text eol=crlf diff --git a/.github/image/logo.png b/.github/image/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a9cdaa37b4314105f5b4663bc274384a7cba552d GIT binary patch literal 52737 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 GGywn&neP7p literal 0 HcmV?d00001 diff --git a/.gitignore b/.gitignore index fcbc20f3..985d4c2c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ build/ .settings .DS_Store .idea/ -alpaca.properties +*.properties +!*.default.properties diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d54435b1..00000000 --- a/.travis.yml +++ /dev/null @@ -1,24 +0,0 @@ -language: java -install: true -dist: trusty - -jdk: - - oraclejdk8 - -before_script: - - chmod +x gradlew - -script: - - ./gradlew build check jacocoTestReport --stacktrace - -after_success: - - bash <(curl -s https://codecov.io/bash) - -before_cache: - - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock - - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ - -cache: - directories: - - $HOME/.gradle/caches/ - - $HOME/.gradle/wrapper/ diff --git a/LICENSE b/LICENSE.txt similarity index 96% rename from LICENSE rename to LICENSE.txt index 706b1675..38f23352 100644 --- a/LICENSE +++ b/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) Jacob Peterson 2021 +Copyright (c) Jacob Peterson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index c483ee2d..94b7e833 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,13 @@ -

Logo

+

Logo

+ GitHub Repository Maven Central Javadocs - Build Status - CodeCov badge GitHub

# Overview -This is a Java implementation for the Alpaca API. Alpaca lets you trade with algorithms, connect with apps, and build services all with a commission-free stock trading API. This library is community developed and if you have any questions, please ask them on [Github Discussions](https://github.com/Petersoj/alpaca-java/discussions), the [Alpaca Slack #dev-alpaca-java channel](https://alpaca.markets/slack), or on the [Alpaca Forums](https://forum.alpaca.markets/). +This library is a Java client implementation of the Alpaca API. Alpaca lets you trade with algorithms, connect with apps, and build services all with a commission-free trading API for stocks, crypto, and options. This library uses the [Alpaca OpenAPI Specifications](https://docs.alpaca.markets/v1.1/openapi) to generate REST API clients and implements the websocket streaming interface using [OkHttp](https://square.github.io/okhttp/). This library is community developed and if you have any questions, please ask them on [Github Discussions](https://github.com/Petersoj/alpaca-java/discussions), the [Alpaca Slack #dev-alpaca-java channel](https://alpaca.markets/slack), or on the [Alpaca Forums](https://forum.alpaca.markets/). Give this repository a star ⭐ if it helped you build a trading algorithm in Java! @@ -16,19 +15,16 @@ Give this repository a star ⭐ if it helped you build a trading algorithm in Ja If you are using Gradle as your build tool, add the following dependency to your `build.gradle` file: ``` -dependencies { - implementation group: 'net.jacobpeterson', name: 'alpaca-java', version: '9.2.0' -} +implementation group: "net.jacobpeterson.alpaca", name: "alpaca-java", version: "10.0.0" ``` If you are using Maven as your build tool, add the following dependency to your `pom.xml` file: ``` - net.jacobpeterson + net.jacobpeterson.alpaca alpaca-java - 9.2.0 - compile + 10.0.0 ``` @@ -516,8 +512,8 @@ newsWebsocket.subscribeToControl( newsWebsocket.connect(); newsWebsocket.waitForAuthorization(5, TimeUnit.SECONDS); if (!newsWebsocket.isValid()) { - System.out.println("Websocket not valid!"); - return; + System.out.println("Websocket not valid!"); + return; } newsWebsocket.subscribe(null, null, null, Arrays.asList("*")); @@ -534,31 +530,18 @@ To build this project yourself, clone this repository and run: ./gradlew build ``` -To install built artifacts to your local maven repo, run: -``` -./gradlew install -x test -``` - -# Testing - -To run mocked tests using Mockito, run: -``` -./gradlew test -``` -Note that mocked tests never send real API requests to Alpaca. Mocked tests are meant to test the basic integrity of this API locally. - -To run live endpoint tests with Alpaca Paper credentials, create the `alpaca.properties` file in `src/test/resources` with the corresponding credentials. Then run: +To install the built artifacts to your local Maven repository on your machine (the `~/.m2/` directory), run: ``` -./gradlew test -PtestPackage=live +./gradlew install ``` -Note that the live tests will modify your account minimally. It's meant to test live endpoints on a real paper account during market hours to confirm that API methods are working properly. Please read through the live endpoint tests [here](https://github.com/Petersoj/alpaca-java/tree/master/src/test/java/live/net/jacobpeterson/alpaca) before running this testing suite on your own paper account. # TODO -- Finish Unit Testing (both live and mocked) -- Use [TA4j](https://github.com/ta4j/ta4j) `Num` interface instead of `Double` for number variables so that users can use either `Double` or `BigDecimal` for performance or precision in price data. -- Add [TimeSeriesDataStore](https://github.com/Petersoj/TimeSeriesDataStore) using Alpaca Data API +- Implement Unit Testing for REST API and Websocket streaming (both live and mocked) +- Implement Broker API websockets # Contributing Contributions are welcome! -If you are creating a Pull Request, be sure to create a new branch in your forked repository for your feature or bug fix instead of committing directly to the `master` branch in your fork. +When creating a Pull Request, keep the following in mind: +1. Create a new branch in your forked repository for your feature or bug fix instead of committing directly to the `master` branch in your fork. +2. Use the `dev` branch in this repository as the base branch in your Pull Request. diff --git a/_config.yml b/_config.yml index 259a24e4..72d781d7 100644 --- a/_config.yml +++ b/_config.yml @@ -1 +1 @@ -theme: jekyll-theme-tactile \ No newline at end of file +theme: jekyll-theme-tactile diff --git a/build.gradle b/build.gradle index a861d055..844d41b9 100644 --- a/build.gradle +++ b/build.gradle @@ -1,35 +1,29 @@ -import org.jsonschema2pojo.gradle.GenerateJsonSchemaJavaTask +import org.openapitools.generator.gradle.plugin.tasks.GenerateTask -buildscript { - repositories { - mavenCentral() - } +plugins { + id "java" + id "java-library" - dependencies { - // For Maven and Maven Central support - classpath group: 'com.bmuschko', name: 'gradle-nexus-plugin', version: '2.3.1' - // For JSONSchema2POJO - classpath group: 'org.jsonschema2pojo', name: 'jsonschema2pojo-gradle-plugin', version: '1.1.1' - } -} + // Adapted JSONSchema2POJO plugin + id "jsonschema2pojo-adapted" -plugins { - id 'java' + // OpenAPI Generator + id "org.openapi.generator" version "7.3.0" - // For Maven and Maven Central support - id 'maven-publish' - id 'com.bmuschko.nexus' version '2.3.1' - id 'io.codearte.nexus-staging' version '0.11.0' + // Gradle versions plugin + id "com.github.ben-manes.versions" version "0.51.0" - // For code coverage reports - id 'jacoco' + // For Maven Central publishing + id "maven-publish" + id "signing" } -apply plugin: 'jsonschema2pojo' +final def projectGroup = "net.jacobpeterson.alpaca" +final def projectArtifactID = "alpaca-java" +final def projectVersion = "10.0.0-SNAPSHOT" -archivesBaseName = 'alpaca-java' -group = 'net.jacobpeterson' -version = '9.2.0-SNAPSHOT' +group = projectGroup +version = projectVersion repositories { mavenCentral() @@ -37,215 +31,237 @@ repositories { dependencies { // Logging framework - implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.36' - // This is a bridge for the Apache Jakarta Commons Logging library used in dependencies to actually use SLF4j instead - implementation group: 'org.slf4j', name: 'jcl-over-slf4j', version: '1.7.36' + implementation group: "org.slf4j", name: "slf4j-api", version: "2.1.0-alpha1" + // Require dependencies that use the Apache Jakarta Commons Logging library to use SLF4j instead + implementation group: "org.slf4j", name: "jcl-over-slf4j", version: "2.1.0-alpha1" + + // Google Guava + implementation group: "com.google.guava", name: "guava", version: "33.0.0-jre" - // Guava and GSON - implementation group: 'com.google.guava', name: 'guava', version: '31.1-jre' - implementation group: 'com.google.code.gson', name: 'gson', version: '2.9.0' + // GSON + implementation group: "com.google.code.gson", name: "gson", version: "2.10.1" // OkHttp - implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.10.0' - implementation group: 'com.github.devcsrj', name: 'slf4j-okhttp3-logging-interceptor', version: '1.0.1' + implementation group: "com.squareup.okhttp3", name: "okhttp", version: "5.0.0-alpha.12" + implementation group: "com.squareup.okhttp3", name: "logging-interceptor", version: "5.0.0-alpha.12" - // Unit test dependencies - testImplementation group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.36' - testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter', version: '5.8.2' - testImplementation group: 'org.mockito', name: 'mockito-core', version: '4.6.1' + // Libraries used by the OpenAPI generated client libraries + implementation group: "jakarta.annotation", name: "jakarta.annotation-api", version: "1.3.5" + implementation group: "org.openapitools", name: "jackson-databind-nullable", version: "0.2.6" + implementation group: "io.gsonfire", name: "gson-fire", version: "1.9.0" } // Exclude any SLF4j-implementation transitive dependencies so users can use a logging framework of their choice configurations.implementation { - exclude group: 'commons-logging', module: 'commons-logging' + exclude group: "commons-logging", module: "commons-logging" } -sourceSets { - main { - java { - srcDirs 'src/main/java' - } - } - test { - java { - srcDirs 'src/test/java' - } - } +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + withJavadocJar() + withSourcesJar() } -sourceCompatibility = 1.8 -targetCompatibility = 1.8 -[compileJava, compileTestJava]*.options*.encoding = 'UTF-8' +[compileJava, compileTestJava]*.options*.encoding = "UTF-8" javadoc { - // Explicitly define source version - options.addStringOption('source', '8') - // Used to suppress Javadoc linting warnings - options.addStringOption('Xdoclint:none', '-quiet') - // Explicitly define Charset for Javadocs - options.addStringOption('charset', 'UTF-8') - // Add Java SE 8 link - options.addStringOption('link', 'https://docs.oracle.com/javase/8/docs/api/') + options.addStringOption("source", "17") + options.addStringOption("Xdoclint:none", "-quiet") // Suppress Javadoc linting warnings + options.addStringOption("charset", "UTF-8") + options.addStringOption("link", "https://docs.oracle.com/javase/8/docs/api/") } -test { - useJUnitPlatform() - - testLogging { - events "passed", "skipped", "failed" - exceptionFormat "full" - } +// +// BEGIN Alpaca OpenAPI Specification (OAS) client generation +// - doFirst { - def testPackageArgument = "testPackage" - filter.includeTestsMatching(project.hasProperty(testPackageArgument) && - project.getProperty(testPackageArgument).equals("live") ? "*.live.*" : "*.mock.*") +// These IDs come from the URLs on this page: https://docs.alpaca.markets/v1.1/openapi +final def specIDsOfFileNames = Map.of( + "trader.json", "657760422129f9005cf4bd58", + "broker.json", "657771fc2471d20070183049", + "market-data.json", "6577704d1fb6b9007032bf39") +final File specDownloadPath = new File(project.layout.buildDirectory.get().getAsFile(), + "/download/openapi/") +final File generatedClientLibrariesPath = new File(project.layout.buildDirectory.get().getAsFile(), + "/generated/openapi/") + +final def generateAllOpenAPIClientsTasks = tasks.register("generateAllOpenAPIClients") { + doLast { + fixOpenAPIGeneratedClientIssues(generatedClientLibrariesPath) } } +compileJava.dependsOn generateAllOpenAPIClientsTasks -jacocoTestReport { - reports { - xml.enabled true - html.enabled false +def downloadOpenAPISpecFilesTask = tasks.register("downloadOpenAPISpecFiles") { + configure { + outputs.dir(specDownloadPath) } -} -jacocoTestReport.dependsOn check -// -// START publishing -// -modifyPom { - project { - name = 'alpaca-java' - description = 'Java API for Alpaca' - url = 'https://github.com/Petersoj/alpaca-java' - inceptionYear = '2018' - - licenses { - license { - name = 'MIT License' - url = 'https://opensource.org/licenses/MIT' + doLast { + // Download spec files + specIDsOfFileNames.forEach { fileName, alpacaSpecID -> + final def outputFile = new File(specDownloadPath, fileName) + final def specURL = "https://docs.alpaca.markets/v1.1/openapi/${alpacaSpecID}" + outputFile.getParentFile().mkdirs() + try { + logger.info("Downloading OpenAPI spec file from: {}", specURL) + new URL(specURL).withInputStream { inputStream -> outputFile.withOutputStream { it << inputStream } } + logger.info("Successfully downloaded OpenAPI spec file to: {}", outputFile.getPath()) + } catch (IOException ioException) { + throw new RuntimeException("Could not download OpenAPI spec file!", ioException) } } - developers { - developer { - id = 'Petersoj' - name = 'Jacob Peterson' - } + // Fix an issue in the Broker spec file that uses an empty string as an object key for some reason + // TODO remove this when issue is fixed by Alpaca + final def brokerSpecFile = new File(specDownloadPath, "broker.json") + brokerSpecFile.text = brokerSpecFile.text + .replace(",\"\":{\"type\":\"string\",\"x-stoplight\":{\"id\":\"1zg5jnb6rzdoo\"}}", "") + } +} + +for (def specFileName : specIDsOfFileNames.keySet()) { + final def specName = specFileName.replace(".json", "") + final def inputSpecFile = new File(specDownloadPath, specFileName) + final def outputDirectory = new File(generatedClientLibrariesPath, specName) - developer { - id = 'mainstringargs' - name = 'main(String[] args)' + // Add source set of OpenAPI generated client library + sourceSets { + main { + java { + srcDir new File(outputDirectory, "src/main/java/").getPath() } } + } - scm { - url = "https://github.com/Petersoj/alpaca-java.git" - connection = "scm:git:https://github.com/Petersoj/alpaca-java.git" - developerConnection = "scm:git:git@github.com/Petersoj/alpaca-java.git" + // Register and configure OpenAPI generate task + final def generateTask = tasks.register("generateOpenAPIClientFor-" + specName, GenerateTask) { + dependsOn { + downloadOpenAPISpecFilesTask } - } -} -extraArchive { - sources = true - tests = true - javadoc = true -} + it.onlyIf { + !downloadOpenAPISpecFilesTask.get().state.upToDate && + downloadOpenAPISpecFilesTask.get().state.failure == null && + !downloadOpenAPISpecFilesTask.get().state.skipped && + downloadOpenAPISpecFilesTask.get().state.executed + } -nexus { - sign = true - repositoryUrl = 'https://oss.sonatype.org/service/local/staging/deploy/maven2/' - snapshotRepositoryUrl = 'https://oss.sonatype.org/content/repositories/snapshots/' -} + configure { + inputs.file(inputSpecFile) + outputs.dir(outputDirectory) + } -nexusStaging { - packageGroup = "net.jacobpeterson" + generatorName.set("java") + inputSpec.set(inputSpecFile.getPath()) + outputDir.set(outputDirectory.getPath()) + final def destinationPackage = projectGroup + ".openapi." + specName.replace("-", "") + configOptions.set(Map.of( + "artifactId", specName, + "artifactVersion", "1.0.0-SNAPSHOT", + "groupId", destinationPackage, + "invokerPackage", destinationPackage, + "apiPackage", destinationPackage + ".api", + "modelPackage", destinationPackage + ".model", + "library", "okhttp-gson", + "documentationProvider", "none")) + cleanupOutput.set(true) + validateSpec.set(false) + skipValidateSpec.set(true) + generateModelTests.set(false) + generateModelDocumentation.set(false) + generateApiTests.set(false) + generateApiDocumentation.set(false) + } + generateAllOpenAPIClientsTasks.configure { + dependsOn generateTask + } } -// Used to prevent accidental publication of Alpaca keys -processResources { - exclude('alpaca.properties') +static void fixOpenAPIGeneratedClientIssues(File generatedClientLibrariesPath) { + // Fix broker spec generated client issues with discriminators + // on some enums: https://github.com/OpenAPITools/openapi-generator/issues/806 + final def brokerTransferDataJavaFile = new File(generatedClientLibrariesPath, + "broker/src/main/java/net/jacobpeterson/alpaca/openapi/broker/model/TransferData.java") + brokerTransferDataJavaFile.text = brokerTransferDataJavaFile.text + .replace("this.transferType = this.getClass().getSimpleName();", "") + final def brokerTransferResourceJavaFile = new File(generatedClientLibrariesPath, + "broker/src/main/java/net/jacobpeterson/alpaca/openapi/broker/model/TransferResource.java") + brokerTransferResourceJavaFile.text = brokerTransferResourceJavaFile.text + .replace("this.type = this.getClass().getSimpleName();", "") } // -// END publishing +// END Alpaca OpenAPI Specification (OAS) client generation // // -// START POJO generation +// BEGIN Publishing // -// Disable default 'jsonSchema2Pojo' task -tasks.getByName("generateJsonSchema2Pojo").enabled(false) - -// The generated POJOs will be in a package structure analogous to the path in the 'schema_json/' directory -// See: https://github.com/joelittlejohn/jsonschema2pojo/wiki/Reference - -final def targetDirectoryPath = file("${project.buildDir}/generated-sources/schemajson").getPath() - -task generatePOJOs() { - final def jsonSourceDirectory = file("${project.projectDir}/schema_json").getPath() - final def jsonPackageNamePrefix = "net.jacobpeterson.alpaca.model" - - // Loop through all files in schema JSON file tree - file(jsonSourceDirectory).eachFileRecurse { jsonFile -> - if (jsonFile.getName().endsWith(".json")) { - def startPackageIndex = jsonFile.getAbsolutePath().indexOf(jsonSourceDirectory) + jsonSourceDirectory.length() - def targetPackage = jsonPackageNamePrefix + jsonFile.getParentFile().getAbsolutePath() - .substring(startPackageIndex) - .replace(File.separator, '.').replace('-', '').replace('_', '') - .toLowerCase() - - def jsonToPOJOTask = tasks.create('json2POJOTask-' + targetPackage + "." + jsonFile.getName(), - GenerateJsonSchemaJavaTask) - - // Configure this task to participate in incremental builds so that it only executes when changes occur - jsonToPOJOTask.configure { - inputs.file(jsonFile) - outputs.dir(targetDirectoryPath) +publishing { + publications { + mavenJava(MavenPublication) { + groupId = projectGroup + artifactId = projectArtifactID + version = projectVersion + + pom { + name = projectArtifactID + description = "A Java API for Alpaca, the commission free, algo friendly, stock trading broker." + url = "https://github.com/Petersoj/alpaca-java" + inceptionYear = "2018" + + licenses { + license { + name = "MIT License" + url = "https://opensource.org/licenses/MIT" + } + } + + developers { + developer { + id = "Petersoj" + name = "Jacob Peterson" + } + developer { + id = "mainstringargs" + name = "main(String[] args)" + } + } + + scm { + url = "https://github.com/Petersoj/alpaca-java.git" + connection = "scm:git:https://github.com/Petersoj/alpaca-java.git" + developerConnection = "scm:git:git@github.com/Petersoj/alpaca-java.git" + } } + } + } - jsonToPOJOTask.doFirst { - jsonToPOJOTask.configuration.source = files(jsonFile.getAbsolutePath()) - jsonToPOJOTask.configuration.targetDirectory = file(targetDirectoryPath) - jsonToPOJOTask.configuration.targetPackage = targetPackage + repositories { + maven { + name = "OSSRH" + url = projectVersion.contains("SNAPSHOT") ? "https://oss.sonatype.org/content/repositories/snapshots/" : + "https://oss.sonatype.org/service/local/staging/deploy/maven2/" + credentials { + username = project.property("nexus.username") + password = project.property("nexus.password") } - - dependsOn jsonToPOJOTask } } } -compileJava { - dependsOn generatePOJOs +signing { + sign publishing.publications.mavenJava } -sourceSets { - main { - java { - srcDir targetDirectoryPath - } - } -} - -jsonSchema2Pojo { - includeAdditionalProperties = true - targetDirectory = file(targetDirectoryPath) - propertyWordDelimiters = ['-', '_'] as char[] - annotationStyle = 'gson' - sourceType = 'jsonschema' - customDateTimePattern = "yyyy-MM-ddTHH:mm:ssZ" - includeGeneratedAnnotation = false - includeConstructors = true - serializable = true - includeGetters = true - includeSetters = true - includeCopyConstructor = true - generateBuilders = true +// Used to prevent accidental publication of Alpaca keys +processResources { + exclude("alpaca.properties") } // -// END POJO generation +// END Publishing // diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 00000000..1ca86d32 --- /dev/null +++ b/buildSrc/build.gradle @@ -0,0 +1,29 @@ +plugins { + id "groovy-gradle-plugin" +} + +gradlePlugin { + plugins { + jsonschema2pojoAdapted { + id = "jsonschema2pojo-adapted" + implementationClass = + "net.jacobpeterson.alpaca.buildsrc.plugin.jsonschema2pojoadapted.JSONSchema2POJOAdaptedPlugin" + } + } +} + +repositories { + mavenCentral() + gradlePluginPortal() +} + +dependencies { + gradleApi() + + // Google Guava + implementation group: "com.google.guava", name: "guava", version: "33.0.0-jre" + + // JsonSchema2Pojo + implementation group: "org.jsonschema2pojo", name: "jsonschema2pojo-core", version: "1.2.1" + implementation group: "org.jsonschema2pojo", name: "jsonschema2pojo-gradle-plugin", version: "1.2.1" +} diff --git a/buildSrc/src/main/groovy/net/jacobpeterson/alpaca/buildsrc/plugin/jsonschema2pojoadapted/GeneratePOJOsTask.groovy b/buildSrc/src/main/groovy/net/jacobpeterson/alpaca/buildsrc/plugin/jsonschema2pojoadapted/GeneratePOJOsTask.groovy new file mode 100644 index 00000000..88ac5a4a --- /dev/null +++ b/buildSrc/src/main/groovy/net/jacobpeterson/alpaca/buildsrc/plugin/jsonschema2pojoadapted/GeneratePOJOsTask.groovy @@ -0,0 +1,128 @@ +package net.jacobpeterson.alpaca.buildsrc.plugin.jsonschema2pojoadapted + +import org.gradle.api.DefaultTask +import org.gradle.api.model.ReplacedBy +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.TaskAction +import org.jsonschema2pojo.GenerationConfig +import org.jsonschema2pojo.Jsonschema2Pojo +import org.jsonschema2pojo.gradle.GradleRuleLogger + +import java.nio.file.Files +import java.nio.file.Path + +import static JSONSchema2POJOAdaptedPlugin.EXTENSION_KEY +import static com.google.common.base.Preconditions.checkArgument + +/** + * {@link GeneratePOJOsTask} is a Gradle task that invokes jsonschema2pojo with special configurations + * and generation logic. + * TODO publish this plugin somewhere so the code doesn't have to be copied between 'cli' and 'system' Gradle projects. + * @see GenerateJsonSchemaJavaTask + */ +class GeneratePOJOsTask extends DefaultTask { + + @ReplacedBy("configurationString") + GenerationConfig configuration + + @Input + String getConfigurationString() { + configuration.toString() + } + + @Internal + List jsonSchemaFileConfigs + + GeneratePOJOsTask() { + group = "Build" + description = "Generates Java POJOs from JSON schemas." + + project.afterEvaluate { + // Validate plugin configuration + configuration = project.extensions.getByName(EXTENSION_KEY) as GenerationConfig + checkArgument(!configuration.source.hasNext(), "'source' should not be set for '${name}'.") + checkArgument(configuration.targetDirectory == null, "'targetDirectory' should not be set for '${name}'.") + + // Set custom defaults on plugin configuration + configuration.targetDirectory = project.file( + "${project.layout.buildDirectory.getAsFile().get().getPath()}/generated-sources/jsonschema2pojo") + configuration.targetPackage = configuration.targetPackage == "" ? + "${project.group}.model" : configuration.targetPackage + configuration.propertyWordDelimiters = ["-", "_"] as char[] + configuration.annotationStyle = "gson" + configuration.sourceType = "jsonschema" + configuration.customDateTimePattern = "yyyy-MM-ddTHH:mm:ssZ" + configuration.includeConstructors = true + configuration.serializable = true + configuration.includeGetters = true + configuration.includeSetters = true + configuration.includeCopyConstructor = true + configuration.generateBuilders = true + + // Get project main java source set + final def projectMainJavaSourceSet = + project.extensions.getByType(SourceSetContainer).named("main").get().java + + // Set task dependents and dependencies + project.tasks.named("compileJava").get().dependsOn(this) + dependsOn(project.tasks.named("processResources").get()) + + // Walk through source files to create a list of JSON schema files to process + jsonSchemaFileConfigs = new ArrayList<>() + projectMainJavaSourceSet.srcDirs.each { srcDir -> + if (!srcDir.exists()) { + return + } + final def sourceDirPath = srcDir.getAbsolutePath() + srcDir.eachFileRecurse { sourceFile -> + final def sourceFilePath = sourceFile.getAbsolutePath() + if (sourceFilePath.endsWith(".json")) { + def targetPackage = sourceFile.getParentFile().getAbsolutePath() + .substring(sourceDirPath.length()) + .replace(File.separator, ".").replace("-", "").replace("_", "") + .toLowerCase() + targetPackage = targetPackage.replace("${project.group}", "") + targetPackage = targetPackage.startsWith(".") ? targetPackage.substring(1) : targetPackage + targetPackage = configuration.targetPackage + + (targetPackage.isEmpty() ? "" : targetPackage) + jsonSchemaFileConfigs.add(new JSONSchemaFileConfig(sourceFile, targetPackage)) + + inputs.file(sourceFile) + outputs.file(project.file(configuration.targetDirectory.getAbsolutePath() + File.separator + + targetPackage.replace(".", File.separator) + File.separator + + sourceFile.getName().replace(".json", ".java"))) + } + } + } + + outputs.dir(configuration.targetDirectory) + outputs.cacheIf { true } + + projectMainJavaSourceSet.srcDirs(configuration.targetDirectory) + } + } + + @TaskAction + def execute() { + logger.info("Deleting existing generated POJOs...") + Files.walk(configuration.targetDirectory.toPath()) + .sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .peek(file -> logger.info("Deleting file: {}", file.getPath())) + .forEach(File::delete) + logger.info("Deleted existing generated POJOs.") + + logger.info("Generating POJOs from JSON schemas...") + final def gradleRuleLogger = new GradleRuleLogger(logger) + jsonSchemaFileConfigs.forEach { jsonSchemaFileConfig -> + configuration.source = List.of(jsonSchemaFileConfig.sourceFile.toURI().toURL()) + configuration.targetPackage = jsonSchemaFileConfig.targetPackage + Jsonschema2Pojo.generate(configuration, gradleRuleLogger) + logger.info("Generated POJO for: {}", jsonSchemaFileConfig.targetPackage + "." + + jsonSchemaFileConfig.sourceFile.getName()) + } + logger.info("Generated POJOs from JSON schemas.") + } +} diff --git a/buildSrc/src/main/groovy/net/jacobpeterson/alpaca/buildsrc/plugin/jsonschema2pojoadapted/JSONSchema2POJOAdaptedPlugin.groovy b/buildSrc/src/main/groovy/net/jacobpeterson/alpaca/buildsrc/plugin/jsonschema2pojoadapted/JSONSchema2POJOAdaptedPlugin.groovy new file mode 100644 index 00000000..9d53a826 --- /dev/null +++ b/buildSrc/src/main/groovy/net/jacobpeterson/alpaca/buildsrc/plugin/jsonschema2pojoadapted/JSONSchema2POJOAdaptedPlugin.groovy @@ -0,0 +1,26 @@ +package net.jacobpeterson.alpaca.buildsrc.plugin.jsonschema2pojoadapted + +import org.gradle.api.GradleException +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.jsonschema2pojo.gradle.JsonSchemaExtension + +/** + * {@link JSONSchema2POJOAdaptedPlugin} is a Gradle plugin that adapts the jsonschema2pojo Gradle plugin + * to add/change some features. + */ +class JSONSchema2POJOAdaptedPlugin implements Plugin { + + public static final EXTENSION_KEY = "jsonSchema2POJOAdaptedConfig" + + @Override + void apply(Project project) { + project.extensions.create(EXTENSION_KEY, JsonSchemaExtension) + if (project.plugins.hasPlugin("java")) { + final def generatePOJOsTask = project.tasks.register("generatePOJOs", GeneratePOJOsTask) + generatePOJOsTask.get().enabled = true + } else { + throw new GradleException("'java' Gradle plugin required.") + } + } +} diff --git a/buildSrc/src/main/groovy/net/jacobpeterson/alpaca/buildsrc/plugin/jsonschema2pojoadapted/JSONSchemaFileConfig.groovy b/buildSrc/src/main/groovy/net/jacobpeterson/alpaca/buildsrc/plugin/jsonschema2pojoadapted/JSONSchemaFileConfig.groovy new file mode 100644 index 00000000..698a30b9 --- /dev/null +++ b/buildSrc/src/main/groovy/net/jacobpeterson/alpaca/buildsrc/plugin/jsonschema2pojoadapted/JSONSchemaFileConfig.groovy @@ -0,0 +1,31 @@ +package net.jacobpeterson.alpaca.buildsrc.plugin.jsonschema2pojoadapted + +/** + * {@link JSONSchemaFileConfig} provides the configuration fields for a single JSON schema file. + */ +class JSONSchemaFileConfig { + + private File sourceFile + private String targetPackage + private String outputFilePath + + /** + * Instantiates a new {@link JSONSchemaFileConfig}. + * + * @param sourceFile the source file + * @param targetPackage the target package + */ + JSONSchemaFileConfig(File sourceFile, String targetPackage) { + this.sourceFile = sourceFile + this.targetPackage = targetPackage + this.outputFilePath = outputFilePath + } + + File getSourceFile() { + return sourceFile + } + + String getTargetPackage() { + return targetPackage + } +} diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index c749985f..00000000 --- a/codecov.yml +++ /dev/null @@ -1,6 +0,0 @@ -coverage: - status: - # Turn off Codecov status for now - project: off - patch: off -comment: false \ No newline at end of file diff --git a/gradle/wrapper/.gitignore b/gradle/wrapper/.gitignore new file mode 100644 index 00000000..351625b1 --- /dev/null +++ b/gradle/wrapper/.gitignore @@ -0,0 +1 @@ +!gradle-wrapper.properties diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1f3fdbc5..2ea3535d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/schema_json/endpoint/account/account.json b/schema_json/endpoint/account/account.json deleted file mode 100644 index 7e558fe2..00000000 --- a/schema_json/endpoint/account/account.json +++ /dev/null @@ -1,110 +0,0 @@ -{ - "type": "object", - "title": "See Account.", - "properties": { - "id": { - "existingJavaType": "java.lang.String", - "title": "Account ID." - }, - "account_number": { - "existingJavaType": "java.lang.String", - "title": "Account number." - }, - "status": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.account.enums.AccountStatus", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.account.enums.AccountStatus}." - }, - "currency": { - "existingJavaType": "java.lang.String", - "title": "\"USD\"" - }, - "cash": { - "existingJavaType": "java.lang.String", - "title": "Cash balance." - }, - "portfolio_value": { - "existingJavaType": "java.lang.String", - "title": "Total value of cash + holding positions. (This field is deprecated. It is equivalent to the equity field.)" - }, - "pattern_day_trader": { - "existingJavaType": "java.lang.Boolean", - "title": "Whether or not the account has been flagged as a pattern day trader." - }, - "trade_suspended_by_user": { - "existingJavaType": "java.lang.Boolean", - "title": "User setting. If true, the account is not allowed to place orders." - }, - "trading_blocked": { - "existingJavaType": "java.lang.Boolean", - "title": "If true, the account is not allowed to place orders." - }, - "transfers_blocked": { - "existingJavaType": "java.lang.Boolean", - "title": "If true, the account is not allowed to request money transfers." - }, - "account_blocked": { - "existingJavaType": "java.lang.Boolean", - "title": "If true, the account activity by user is prohibited." - }, - "created_at": { - "existingJavaType": "java.time.ZonedDateTime", - "title": "Timestamp this account was created at." - }, - "shorting_enabled": { - "existingJavaType": "java.lang.Boolean", - "title": "Flag to denote whether or not the account is permitted to short." - }, - "long_market_value": { - "existingJavaType": "java.lang.String", - "title": "Realtime MtM value of all long positions held in the account." - }, - "short_market_value": { - "existingJavaType": "java.lang.String", - "title": "Realtime MtM value of all short positions held in the account." - }, - "equity": { - "existingJavaType": "java.lang.String", - "title": "Cash + long_market_value + short_market_value." - }, - "last_equity": { - "existingJavaType": "java.lang.String", - "title": "Equity as of previous trading day at 16:00:00 ET." - }, - "multiplier": { - "existingJavaType": "java.lang.String", - "title": "Buying power multiplier that represents account margin classification; valid values 1 (standard limited margin account with 1x buying power), 2 (reg T margin account with 2x intraday and overnight buying power; this is the default for all non-PDT accounts with $2,000 or more equity), 4 (PDT account with 4x intraday buying power and 2x reg T overnight buying power)." - }, - "buying_power": { - "existingJavaType": "java.lang.String", - "title": "Current available $ buying power; If multiplier = 4, this is your daytrade buying power which is calculated as (last_equity - (last) maintenance_margin) * 4; If multiplier = 2, buying_power = max(equity – initial_margin,0) * 2; If multiplier = 1, buying_power = cash." - }, - "initial_margin": { - "existingJavaType": "java.lang.String", - "title": "Reg T initial margin requirement (continuously updated value)." - }, - "maintenance_margin": { - "existingJavaType": "java.lang.String", - "title": "Maintenance margin requirement (continuously updated value)." - }, - "sma": { - "existingJavaType": "java.lang.String", - "title": "Value of special memorandum account (will be used at a later date to provide additional buying_power)." - }, - "daytrade_count": { - "existingJavaType": "java.lang.Integer", - "title": "The current number of daytrades that have been made in the last 5 trading days (inclusive of today)." - }, - "last_maintenance_margin": { - "existingJavaType": "java.lang.String", - "title": "Your maintenance margin requirement on the previous trading day." - }, - "daytrading_buying_power": { - "existingJavaType": "java.lang.String", - "title": "Your buying power for day trades (continuously updated value)." - }, - "regt_buying_power": { - "existingJavaType": "java.lang.String", - "title": "Your buying power under Regulation T (your excess equity - equity minus margin value - times your margin multiplier)." - } - } -} diff --git a/schema_json/endpoint/account/enums/account_status.json b/schema_json/endpoint/account/enums/account_status.json deleted file mode 100644 index 61f7a081..00000000 --- a/schema_json/endpoint/account/enums/account_status.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "type": "string", - "title": "See Account.", - "enum": [ - "ONBOARDING", - "SUBMISSION_FAILED", - "SUBMITTED", - "ACCOUNT_UPDATED", - "APPROVAL_PENDING", - "ACTIVE", - "REJECTED", - "DISABLED", - "ACTION_REQUIRED" - ], - "javaEnums": [ - { - "description": "The account is onboarding." - }, - { - "description": "The account application submission failed for some reason." - }, - { - "description": "The account application has been submitted for review." - }, - { - "description": "The account information is being updated." - }, - { - "description": "The final account approval is pending." - }, - { - "description": "The account is active for trading." - }, - { - "description": "The account application has been rejected." - }, - { - "description": "The account has been disabled." - }, - { - "description": "The account requires manual attention." - } - ] -} diff --git a/schema_json/endpoint/account_activities/account_activity.json b/schema_json/endpoint/account_activities/account_activity.json deleted file mode 100644 index bfa36b8f..00000000 --- a/schema_json/endpoint/account_activities/account_activity.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "object", - "title": "See Account Activities.", - "properties": { - "activity_type": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.accountactivities.enums.ActivityType", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.accountactivities.enums.ActivityType}." - }, - "id": { - "existingJavaType": "java.lang.String", - "title": "An ID for the activity, always in \"::\" format. Can be sent as page_token in requests to facilitate the paging of results." - } - } -} diff --git a/schema_json/endpoint/account_activities/enums/activity_type.json b/schema_json/endpoint/account_activities/enums/activity_type.json deleted file mode 100644 index 9fae432b..00000000 --- a/schema_json/endpoint/account_activities/enums/activity_type.json +++ /dev/null @@ -1,152 +0,0 @@ -{ - "type": "string", - "title": "See Account Activities.", - "enum": [ - "FILL", - "TRANS", - "MISC", - "ACATC", - "ACATS", - "CSD", - "CSR", - "CSW", - "DIV", - "DIVCGL", - "DIVCGS", - "DIVFEE", - "DIVFT", - "DIVNRA", - "DIVROC", - "DIVTW", - "DIVTXEX", - "INT", - "INTNRA", - "INTTW", - "JNL", - "JNLC", - "JNLS", - "MA", - "NC", - "OPASN", - "OPEXP", - "OPXRC", - "PTC", - "PTR", - "FEE", - "REORG", - "SC", - "SSO", - "SSP", - "SPLIT" - ], - "javaEnums": [ - { - "description": "Order fills (both partial and full fills)." - }, - { - "description": "Cash transactions (both CSD and CSW)." - }, - { - "description": "Miscellaneous or rarely used activity types (All types except those in TRANS, DIV, or FILL)." - }, - { - "description": "ACATS IN/OUT (Cash)." - }, - { - "description": "ACATS IN/OUT (Securities)." - }, - { - "description": "Cash deposit(+)." - }, - { - "description": "Cash receipt(-)." - }, - { - "description": "Cash withdrawal(-)." - }, - { - "description": "Dividends." - }, - { - "description": "Dividend (capital gain long term)." - }, - { - "description": "Dividend (capital gain short term)." - }, - { - "description": "Dividend fee." - }, - { - "description": "Dividend adjusted (Foreign Tax Withheld)." - }, - { - "description": "Dividend adjusted (NRA Withheld)." - }, - { - "description": "Dividend return of capital." - }, - { - "description": "Dividend adjusted (Tefra Withheld)." - }, - { - "description": "Dividend (tax exempt)." - }, - { - "description": "Interest (credit/margin)." - }, - { - "description": "Interest adjusted (NRA Withheld)." - }, - { - "description": "Interest adjusted (Tefra Withheld)." - }, - { - "description": "Journal entry." - }, - { - "description": "Journal entry (cash)." - }, - { - "description": "Journal entry (stock)." - }, - { - "description": "Merger/Acquisition." - }, - { - "description": "Name change." - }, - { - "description": "Option assignment." - }, - { - "description": "Option expiration." - }, - { - "description": "Option exercise." - }, - { - "description": "Pass Thru Charge." - }, - { - "description": "Pass Thru Rebate." - }, - { - "description": "REG/TAF fees." - }, - { - "description": "Reorg CA." - }, - { - "description": "Symbol change." - }, - { - "description": "Stock spinoff." - }, - { - "description": "Stock split." - }, - { - "description": "Stock split." - } - ] -} diff --git a/schema_json/endpoint/account_activities/enums/trade_activity_side.json b/schema_json/endpoint/account_activities/enums/trade_activity_side.json deleted file mode 100644 index ddcd4869..00000000 --- a/schema_json/endpoint/account_activities/enums/trade_activity_side.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "type": "string", - "title": "See Account Activities.", - "enum": [ - "buy", - "sell" - ] -} diff --git a/schema_json/endpoint/account_activities/enums/trade_activity_type.json b/schema_json/endpoint/account_activities/enums/trade_activity_type.json deleted file mode 100644 index 8b92e2b9..00000000 --- a/schema_json/endpoint/account_activities/enums/trade_activity_type.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "type": "string", - "title": "See Account Activities.", - "enum": [ - "fill", - "partial_fill" - ] -} diff --git a/schema_json/endpoint/account_activities/non_trade_activity.json b/schema_json/endpoint/account_activities/non_trade_activity.json deleted file mode 100644 index 81d8c0fa..00000000 --- a/schema_json/endpoint/account_activities/non_trade_activity.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "type": "object", - "title": "See Account Activities.", - "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.accountactivities.AccountActivity" - }, - "properties": { - "date": { - "existingJavaType": "java.lang.String", - "title": "The date on which the activity occurred or on which the transaction associated with the activity settled." - }, - "net_amount": { - "existingJavaType": "java.lang.String", - "title": "The net amount of money (positive or negative) associated with the activity." - }, - "symbol": { - "existingJavaType": "java.lang.String", - "title": "The symbol of the security involved with the activity. Not present for all activity types." - }, - "qty": { - "existingJavaType": "java.lang.String", - "javaName": "quantity", - "title": "For dividend activities, the number of shares that contributed to the payment. Not present for other activity types." - }, - "per_share_amount": { - "existingJavaType": "java.lang.String", - "title": "For dividend activities, the average amount paid per share. Not present for other activity types." - }, - "description": { - "existingJavaType": "java.lang.String", - "title": "Description field for transaction records (TRANS, CSR, CSD)." - }, - "status": { - "existingJavaType": "java.lang.String", - "title": "The status." - } - } -} diff --git a/schema_json/endpoint/account_activities/trade_activity.json b/schema_json/endpoint/account_activities/trade_activity.json deleted file mode 100644 index 1697ad1c..00000000 --- a/schema_json/endpoint/account_activities/trade_activity.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "type": "object", - "title": "See Account Activities.", - "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.accountactivities.AccountActivity" - }, - "properties": { - "cum_qty": { - "existingJavaType": "java.lang.String", - "javaName": "cumulativeQuantity", - "title": "The cumulative quantity of shares involved in the execution." - }, - "leaves_qty": { - "existingJavaType": "java.lang.String", - "javaName": "remainingQuantity", - "title": "For partially_filled orders, the quantity of shares that are left to be filled." - }, - "price": { - "existingJavaType": "java.lang.String", - "title": "The per-share price that the trade was executed at." - }, - "qty": { - "existingJavaType": "java.lang.String", - "javaName": "quantity", - "title": "The number of shares involved in the trade execution." - }, - "side": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.accountactivities.enums.TradeActivitySide", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.accountactivities.enums.TradeActivitySide}." - }, - "symbol": { - "existingJavaType": "java.lang.String", - "title": "The symbol of the security being traded." - }, - "transaction_time": { - "existingJavaType": "java.time.ZonedDateTime", - "title": "The time at which the execution occurred." - }, - "order_id": { - "existingJavaType": "java.lang.String", - "title": "The id for the order that filled. Always in GUID format." - }, - "type": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.accountactivities.enums.TradeActivityType", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.accountactivities.enums.TradeActivityType}." - } - } -} diff --git a/schema_json/endpoint/account_configuration/account_configuration.json b/schema_json/endpoint/account_configuration/account_configuration.json deleted file mode 100644 index 704ee4e1..00000000 --- a/schema_json/endpoint/account_configuration/account_configuration.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "type": "object", - "title": "See Account Configuration.", - "properties": { - "dtbp_check": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.accountconfiguration.enums.DTBPCheck", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.accountconfiguration.enums.DTBPCheck}. Controls Day Trading Margin Call (DTMC) checks." - }, - "trade_confirm_email": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.accountconfiguration.enums.TradeConfirmEmail", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.accountconfiguration.enums.TradeConfirmEmail}. If {@link net.jacobpeterson.alpaca.model.endpoint.accountconfiguration.enums.TradeConfirmEmail#NONE}, emails for order fills are not sent." - }, - "suspend_trade": { - "existingJavaType": "java.lang.Boolean", - "title": "If true, new orders are blocked." - }, - "no_shorting": { - "existingJavaType": "java.lang.Boolean", - "title": "If true, account becomes long-only mode." - } - } -} diff --git a/schema_json/endpoint/account_configuration/enums/dtbp_check.json b/schema_json/endpoint/account_configuration/enums/dtbp_check.json deleted file mode 100644 index 75940d81..00000000 --- a/schema_json/endpoint/account_configuration/enums/dtbp_check.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "type": "string", - "javaName": "DTBPCheck", - "title": "See Account Configuration.", - "enum": [ - "both", - "entry", - "exit" - ] -} diff --git a/schema_json/endpoint/account_configuration/enums/trade_confirm_email.json b/schema_json/endpoint/account_configuration/enums/trade_confirm_email.json deleted file mode 100644 index 8f28197c..00000000 --- a/schema_json/endpoint/account_configuration/enums/trade_confirm_email.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "type": "string", - "title": "See Account Configuration.", - "enum": [ - "all", - "none" - ] -} diff --git a/schema_json/endpoint/assets/asset.json b/schema_json/endpoint/assets/asset.json deleted file mode 100644 index fa65e096..00000000 --- a/schema_json/endpoint/assets/asset.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "type": "object", - "title": "See Assets.", - "properties": { - "id": { - "existingJavaType": "java.lang.String", - "title": "Asset ID." - }, - "class": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.assets.enums.AssetClass", - "javaName": "assetClass", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.assets.enums.AssetClass}." - }, - "exchange": { - "existingJavaType": "java.lang.String", - "title": "AMEX, ARCA, BATS, NYSE, NASDAQ or NYSEARCA." - }, - "symbol": { - "existingJavaType": "java.lang.String", - "title": "The symbol." - }, - "name": { - "existingJavaType": "java.lang.String", - "title": "The official name of the asset." - }, - "status": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.assets.enums.AssetStatus", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.assets.enums.AssetStatus}." - }, - "tradable": { - "existingJavaType": "java.lang.Boolean", - "title": "Asset is tradable on Alpaca or not." - }, - "marginable": { - "existingJavaType": "java.lang.Boolean", - "title": "Asset is marginable or not." - }, - "shortable": { - "existingJavaType": "java.lang.Boolean", - "title": "Asset is shortable or not." - }, - "easy_to_borrow": { - "existingJavaType": "java.lang.Boolean", - "title": "Asset is easy-to-borrow or not (filtering for easy_to_borrow = True is the best way to check whether the name is currently available to short at Alpaca)." - }, - "fractionable": { - "existingJavaType": "java.lang.Boolean", - "title": "Asset is fractionable or not." - } - } -} diff --git a/schema_json/endpoint/assets/enums/asset_class.json b/schema_json/endpoint/assets/enums/asset_class.json deleted file mode 100644 index ac36f8e7..00000000 --- a/schema_json/endpoint/assets/enums/asset_class.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "type": "string", - "title": "See Assets.", - "enum": [ - "us_equity", - "crypto" - ] -} diff --git a/schema_json/endpoint/assets/enums/asset_status.json b/schema_json/endpoint/assets/enums/asset_status.json deleted file mode 100644 index b77af48e..00000000 --- a/schema_json/endpoint/assets/enums/asset_status.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "type": "string", - "title": "See Assets.", - "enum": [ - "active", - "inactive" - ] -} diff --git a/schema_json/endpoint/calendar/calendar.json b/schema_json/endpoint/calendar/calendar.json deleted file mode 100644 index 96207ded..00000000 --- a/schema_json/endpoint/calendar/calendar.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "type": "object", - "title": "See Calendar.", - "properties": { - "date": { - "existingJavaType": "java.time.LocalDate", - "title": "Date string in \"%Y-%m-%d\" format." - }, - "open": { - "existingJavaType": "java.time.LocalTime", - "title": "The time the market opens at on this date in \"%H:%M\" format." - }, - "close": { - "existingJavaType": "java.time.LocalTime", - "title": "The time the market closes at on this date in \"%H:%M\" format." - } - } -} diff --git a/schema_json/endpoint/clock/clock.json b/schema_json/endpoint/clock/clock.json deleted file mode 100644 index 671d4a19..00000000 --- a/schema_json/endpoint/clock/clock.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "type": "object", - "title": "See Clock.", - "properties": { - "timestamp": { - "existingJavaType": "java.time.ZonedDateTime", - "title": "Current timestamp." - }, - "is_open": { - "existingJavaType": "java.lang.Boolean", - "title": "Whether or not the market is open." - }, - "next_open": { - "existingJavaType": "java.time.ZonedDateTime", - "title": "Next market open timestamp." - }, - "next_close": { - "existingJavaType": "java.time.ZonedDateTime", - "title": "Next market close timestamp." - } - } -} diff --git a/schema_json/endpoint/common/enums/sort_direction.json b/schema_json/endpoint/common/enums/sort_direction.json deleted file mode 100644 index 01e5a4eb..00000000 --- a/schema_json/endpoint/common/enums/sort_direction.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "type": "string", - "title": "Defines enums for sort directions.", - "enum": [ - "asc", - "desc" - ], - "javaEnums": [ - { - "name": "ASCENDING" - }, - { - "name": "DESCENDING" - } - ] -} diff --git a/schema_json/endpoint/corporate_actions/common/announcement.json b/schema_json/endpoint/corporate_actions/common/announcement.json deleted file mode 100644 index 14c8e66e..00000000 --- a/schema_json/endpoint/corporate_actions/common/announcement.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "type": "object", - "title": "See Corporate announcement.", - "properties": { - "id": { - "existingJavaType": "java.lang.String" - }, - "corporate_actions_id": { - "existingJavaType": "java.lang.String" - }, - "ca_type": { - "existingJavaType": "java.lang.String" - }, - "ca_sub_type": { - "existingJavaType": "java.lang.String" - }, - "initiating_symbol": { - "existingJavaType": "java.lang.String" - }, - "initiating_original_cusip": { - "existingJavaType": "java.lang.String" - }, - "target_symbol": { - "target_symbol": "java.lang.String" - }, - "target_original_cusip": { - "target_original_cusip": "java.lang.String" - }, - "declaration_date": { - "declaration_date": "java.lang.String" - }, - "expiration_date": { - "declaration_date": "java.lang.String" - }, - "record_date": { - "declaration_date": "java.lang.String" - }, - "payable_date": { - "declaration_date": "java.lang.String" - }, - "cash": { - "declaration_date": "java.lang.String" - }, - "old_rate": { - "declaration_date": "java.lang.String" - }, - "new_rate": { - "declaration_date": "java.lang.String" - } - - - } -} diff --git a/schema_json/endpoint/orders/cancelled_order.json b/schema_json/endpoint/orders/cancelled_order.json deleted file mode 100644 index cd34b921..00000000 --- a/schema_json/endpoint/orders/cancelled_order.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "object", - "title": "See Orders.", - "properties": { - "id": { - "existingJavaType": "java.lang.String", - "title": "Order ID." - }, - "status": { - "existingJavaType": "java.lang.Integer", - "title": "The HTTP status code of the cancelled order." - }, - "body": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.orders.Order", - "javaName": "order", - "title": "The cancelled {@link net.jacobpeterson.alpaca.model.endpoint.orders.Order}." - } - } -} diff --git a/schema_json/endpoint/orders/enums/current_order_status.json b/schema_json/endpoint/orders/enums/current_order_status.json deleted file mode 100644 index 25a5829f..00000000 --- a/schema_json/endpoint/orders/enums/current_order_status.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "type": "string", - "title": "See Orders.", - "enum": [ - "open", - "closed", - "all" - ] -} diff --git a/schema_json/endpoint/orders/enums/order_class.json b/schema_json/endpoint/orders/enums/order_class.json deleted file mode 100644 index 6baaf86c..00000000 --- a/schema_json/endpoint/orders/enums/order_class.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "type": "string", - "title": "See Orders.", - "enum": [ - "simple", - "bracket", - "oco", - "oto" - ], - "javaEnums": [ - { - "name": "SIMPLE" - }, - { - "name": "BRACKET" - }, - { - "name": "ONE_CANCELS_OTHER" - }, - { - "name": "ONE_TRIGGERS_OTHER" - } - ] -} diff --git a/schema_json/endpoint/orders/enums/order_side.json b/schema_json/endpoint/orders/enums/order_side.json deleted file mode 100644 index e8b6178f..00000000 --- a/schema_json/endpoint/orders/enums/order_side.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "type": "string", - "title": "See Orders.", - "enum": [ - "buy", - "sell" - ] -} diff --git a/schema_json/endpoint/orders/enums/order_status.json b/schema_json/endpoint/orders/enums/order_status.json deleted file mode 100644 index 5174e120..00000000 --- a/schema_json/endpoint/orders/enums/order_status.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "type": "string", - "title": "See Orders.", - "enum": [ - "new", - "partially_filled", - "filled", - "done_for_day", - "canceled", - "expired", - "replaced", - "pending_cancel", - "pending_replace", - "accepted", - "pending_new", - "accepted_for_bidding", - "stopped", - "rejected", - "suspended", - "calculated", - "held" - ], - "javaEnums": [ - { - "description": "The order has been received by Alpaca, and routed to exchanges for execution. This is the usual initial state of an order." - }, - { - "description": "The order has been partially filled." - }, - { - "description": "The order has been filled, and no further updates will occur for the order." - }, - { - "description": "The order is done executing for the day, and will not receive further updates until the next trading day." - }, - { - "description": "The order has been canceled, and no further updates will occur for the order. This can be either due to a cancel request by the user, or the order has been canceled by the exchanges due to its time-in-force." - }, - { - "description": "The order has expired, and no further updates will occur for the order." - }, - { - "description": "The order was replaced by another order, or was updated due to a market event such as corporate action." - }, - { - "description": "The order is waiting to be canceled." - }, - { - "description": "The order is waiting to be replaced by another order. The order will reject cancel request while in this state." - }, - { - "description": "The order has been received by Alpaca, but hasn’t yet been routed to the execution venue. This could be seen often out side of trading session hours." - }, - { - "description": "The order has been received by Alpaca, and routed to the exchanges, but has not yet been accepted for execution. This state only occurs on rare occasions." - }, - { - "description": "The order has been received by exchanges, and is evaluated for pricing. This state only occurs on rare occasions." - }, - { - "description": "The order has been stopped, and a trade is guaranteed for the order, usually at a stated price or better, but has not yet occurred. This state only occurs on rare occasions." - }, - { - "description": "The order has been rejected, and no further updates will occur for the order. This state occurs on rare occasions and may occur based on various conditions decided by the exchanges." - }, - { - "description": "The order has been suspended, and is not eligible for trading. This state only occurs on rare occasions." - }, - { - "description": "The order has been completed for the day (either filled or done for day), but remaining settlement calculations are still pending. This state only occurs on rare occasions." - }, - { - "description": "The order is part of a bracket leg that is waiting for the initial order to be filled." - } - ] -} diff --git a/schema_json/endpoint/orders/enums/order_time_in_force.json b/schema_json/endpoint/orders/enums/order_time_in_force.json deleted file mode 100644 index fe468cd2..00000000 --- a/schema_json/endpoint/orders/enums/order_time_in_force.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "type": "string", - "title": "See Orders.", - "enum": [ - "day", - "gtc", - "opg", - "cls", - "ioc", - "fok" - ], - "javaEnums": [ - { - "name": "DAY", - "description": "A day order is eligible for execution only on the day it is live. By default, the order is only valid during Regular Trading Hours (9:30am - 4:00pm ET). If unfilled after the closing auction, it is automatically canceled. If submitted after the close, it is queued and submitted the following trading day. However, if marked as eligible for extended hours, the order can also execute during supported extended hours." - }, - { - "name": "GOOD_UNTIL_CANCELLED", - "description": "The order is good until canceled. Non-marketable GTC limit orders are subject to price adjustments to offset corporate actions affecting the issue. We do not currently support Do Not Reduce(DNR) orders to opt out of such price adjustments." - }, - { - "name": "OPG", - "description": "Use this TIF with a market/limit order type to submit \"market on open\" (MOO) and \"limit on open\" (LOO) orders. This order is eligible to execute only in the market opening auction. Any unfilled orders after the open will be cancelled. OPG orders submitted after 9:28am but before 7:00pm ET will be rejected. OPG orders submitted after 7:00pm will be queued and routed to the following day’s opening auction. On open/on close orders are routed to the primary exchange. Such orders do not necessarily execute exactly at 9:30am / 4:00pm ET but execute per the exchange’s auction rules." - }, - { - "name": "CLS", - "description": "Use this TIF with a market/limit order type to submit \"market on close\" (MOC) and \"limit on close\" (LOC) orders. This order is eligible to execute only in the market closing auction. Any unfilled orders after the close will be cancelled. CLS orders submitted after 3:50pm but before 7:00pm ET will be rejected. CLS orders submitted after 7:00pm will be queued and routed to the following day’s closing auction. Only available with API v2." - }, - { - "name": "IMMEDIATE_OR_CANCEL", - "description": "An Immediate Or Cancel (IOC) order requires all or part of the order to be executed immediately. Any unfilled portion of the order is canceled. Only available with API v2. Most market makers who receive IOC orders will attempt to fill the order on a principal basis only, and cancel any unfilled balance. On occasion, this can result in the entire order being cancelled if the market maker does not have any existing inventory of the security in question." - }, - { - "name": "FILL_OR_KILL", - "description": "A Fill or Kill (FOK) order is only executed if the entire order quantity can be filled, otherwise the order is canceled. Only available with API v2." - } - ] -} diff --git a/schema_json/endpoint/orders/enums/order_type.json b/schema_json/endpoint/orders/enums/order_type.json deleted file mode 100644 index 32d93d2b..00000000 --- a/schema_json/endpoint/orders/enums/order_type.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "type": "string", - "title": "See Orders.", - "enum": [ - "market", - "limit", - "stop", - "stop_limit", - "trailing_stop" - ], - "javaEnums": [ - { - "description": "A market order is a request to buy or sell a security at the currently available market price. It provides the most likely method of filling an order. Market orders fill nearly instantaneously." - }, - { - "description": "A limit order is an order to buy or sell at a specified price or better. A buy limit order (a limit order to buy) is executed at the specified limit price or lower (i.e., better). Conversely, a sell limit order (a limit order to sell) is executed at the specified limit price or higher (better). Unlike a market order, you have to specify the limit price parameter when submitting your order." - }, - { - "description": "A stop (market) order is an order to buy or sell a security when its price moves past a particular point, ensuring a higher probability of achieving a predetermined entry or exit price. Once the market price crosses the specified stop price, the stop order becomes a market order. Alpaca converts buy stop orders into stop limit orders with a limit price that is 4% higher than a stop price < $50 (or 2.5% higher than a stop price >= $50). Sell stop orders are not converted into stop limit orders." - }, - { - "description": "A stop-limit order is a conditional trade over a set time frame that combines the features of a stop order with those of a limit order and is used to mitigate risk. The stop-limit order will be executed at a specified limit price, or better, after a given stop price has been reached. Once the stop price is reached, the stop-limit order becomes a limit order to buy or sell at the limit price or better" - }, - { - "description": "Trailing stop orders allow you to continuously and automatically keep updating the stop price threshold based on the stock price movement. You request a single order with a dollar offset value or percentage value as the trail and the actual stop price for this order changes as the stock price moves in your favorable way, or stay at the last level otherwise. This way, you don’t need to monitor the price movement and keep sending replace requests to update the stop price close to the latest market movement." - } - ] -} diff --git a/schema_json/endpoint/orders/order.json b/schema_json/endpoint/orders/order.json deleted file mode 100644 index 6a6f85fd..00000000 --- a/schema_json/endpoint/orders/order.json +++ /dev/null @@ -1,134 +0,0 @@ -{ - "type": "object", - "title": "See Orders.", - "properties": { - "id": { - "existingJavaType": "java.lang.String", - "title": "Order ID." - }, - "client_order_id": { - "existingJavaType": "java.lang.String", - "title": "Client unique order ID." - }, - "created_at": { - "existingJavaType": "java.time.ZonedDateTime", - "title": "Created at." - }, - "updated_at": { - "existingJavaType": "java.time.ZonedDateTime", - "title": "Updated at." - }, - "submitted_at": { - "existingJavaType": "java.time.ZonedDateTime", - "title": "Submitted at." - }, - "filled_at": { - "existingJavaType": "java.time.ZonedDateTime", - "title": "Filled at." - }, - "expired_at": { - "existingJavaType": "java.time.ZonedDateTime", - "title": "Expired at." - }, - "canceled_at": { - "existingJavaType": "java.time.ZonedDateTime", - "title": "Canceled at." - }, - "failed_at": { - "existingJavaType": "java.time.ZonedDateTime", - "title": "Failed at." - }, - "replaced_at": { - "existingJavaType": "java.time.ZonedDateTime", - "title": "Replaced at." - }, - "replaced_by": { - "existingJavaType": "java.lang.String", - "title": "The order ID that this order was replaced by." - }, - "replaces": { - "existingJavaType": "java.lang.String", - "title": "The order ID that this order replaces." - }, - "asset_id": { - "existingJavaType": "java.lang.String", - "title": "Asset ID." - }, - "symbol": { - "existingJavaType": "java.lang.String", - "title": "Asset symbol." - }, - "asset_class": { - "existingJavaType": "java.lang.String", - "title": "Asset class." - }, - "notional": { - "existingJavaType": "java.lang.String", - "title": "Ordered notional amount. If entered, qty will be null. Can take up to 9 decimal points." - }, - "qty": { - "existingJavaType": "java.lang.String", - "javaName": "quantity", - "title": "Ordered quantity. If entered, notional will be null. Can take up to 9 decimal points." - }, - "filled_qty": { - "existingJavaType": "java.lang.String", - "javaName": "filledQuantity", - "title": "Filled quantity." - }, - "filled_avg_price": { - "existingJavaType": "java.lang.String", - "javaName": "averageFillPrice", - "title": "Filled average price." - }, - "order_class": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.orders.enums.OrderClass", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.orders.enums.OrderClass}. For details of non-simple order classes, please see Bracket Order Overview." - }, - "type": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.orders.enums.OrderType", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.orders.enums.OrderType}." - }, - "side": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.orders.enums.OrderSide", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.orders.enums.OrderSide}." - }, - "time_in_force": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.orders.enums.OrderTimeInForce", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.orders.enums.OrderTimeInForce}." - }, - "limit_price": { - "existingJavaType": "java.lang.String", - "title": "Limit price." - }, - "stop_price": { - "existingJavaType": "java.lang.String", - "title": "Stop price." - }, - "status": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.orders.enums.OrderStatus", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.orders.enums.OrderStatus}." - }, - "extended_hours": { - "existingJavaType": "java.lang.Boolean", - "title": "If true, eligible for execution outside regular trading hours." - }, - "legs": { - "existingJavaType": "java.util.ArrayList", - "title": "When querying non-simple order_class orders in a nested style, an {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.orders.Order} entities associated with this order. Otherwise, null." - }, - "trail_percent": { - "existingJavaType": "java.lang.String", - "title": "The percent value away from the high water mark for trailing stop orders." - }, - "trail_price": { - "existingJavaType": "java.lang.String", - "title": "The dollar value away from the high water mark for trailing stop orders." - }, - "hwm": { - "existingJavaType": "java.lang.String", - "javaName": "highWaterMark", - "title": "High Water Mark - The highest (lowest) market price seen since the trailing stop order was submitted." - } - } -} diff --git a/schema_json/endpoint/portfolio_history/enums/portfolio_period_unit.json b/schema_json/endpoint/portfolio_history/enums/portfolio_period_unit.json deleted file mode 100644 index b55c083d..00000000 --- a/schema_json/endpoint/portfolio_history/enums/portfolio_period_unit.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "type": "string", - "title": "See Portfolio History.", - "enum": [ - "D", - "W", - "M", - "A" - ], - "javaEnums": [ - { - "name": "DAY" - }, - { - "name": "WEEK" - }, - { - "name": "MONTH" - }, - { - "name": "YEAR" - } - ] -} diff --git a/schema_json/endpoint/portfolio_history/enums/portfolio_time_frame.json b/schema_json/endpoint/portfolio_history/enums/portfolio_time_frame.json deleted file mode 100644 index 0fb614c7..00000000 --- a/schema_json/endpoint/portfolio_history/enums/portfolio_time_frame.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "type": "string", - "title": "See Portfolio History.", - "enum": [ - "1Min", - "5Min", - "15Min", - "1H", - "1D" - ], - "javaEnums": [ - { - "name": "ONE_MIN" - }, - { - "name": "FIVE_MINUTE" - }, - { - "name": "FIFTEEN_MINUTE" - }, - { - "name": "ONE_HOUR" - }, - { - "name": "ONE_DAY" - } - ] -} diff --git a/schema_json/endpoint/portfolio_history/portfolio_history.json b/schema_json/endpoint/portfolio_history/portfolio_history.json deleted file mode 100644 index f9f833b4..00000000 --- a/schema_json/endpoint/portfolio_history/portfolio_history.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "type": "object", - "title": "See Portfolio History.", - "properties": { - "data_points": { - "existingJavaType": "java.util.ArrayList", - "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.portfoliohistory.PortfolioHistoryDataPoint}s." - }, - "base_value": { - "existingJavaType": "java.lang.Double", - "title": "Basis in dollar of the profit loss calculation." - }, - "timeframe": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.portfoliohistory.enums.PortfolioTimeFrame", - "title": "Time window size of each data element." - } - } -} diff --git a/schema_json/endpoint/portfolio_history/portfolio_history_data_point.json b/schema_json/endpoint/portfolio_history/portfolio_history_data_point.json deleted file mode 100644 index 8cafa239..00000000 --- a/schema_json/endpoint/portfolio_history/portfolio_history_data_point.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "type": "object", - "title": "See Portfolio History.", - "properties": { - "timestamp": { - "existingJavaType": "java.time.ZonedDateTime", - "title": "{@link java.time.ZonedDateTime} of this data element." - }, - "equity": { - "existingJavaType": "java.lang.Double", - "title": "Equity value of the account in dollar amount as of the end of this time window." - }, - "profit_loss": { - "existingJavaType": "java.lang.Double", - "title": "Profit/loss in dollar from the base value." - }, - "profit_loss_percent": { - "existingJavaType": "java.lang.Double", - "title": "Profit/loss in percentage from the base value." - } - } -} diff --git a/schema_json/endpoint/portfolio_history/portfolio_history_response.json b/schema_json/endpoint/portfolio_history/portfolio_history_response.json deleted file mode 100644 index d3ca8177..00000000 --- a/schema_json/endpoint/portfolio_history/portfolio_history_response.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "type": "object", - "title": "See Portfolio History.", - "properties": { - "timestamp": { - "existingJavaType": "java.util.ArrayList", - "title": "Time of each data element, left-labeled (the beginning of time window)." - }, - "equity": { - "existingJavaType": "java.util.ArrayList", - "title": "Equity value of the account in dollar amount as of the end of each time window." - }, - "profit_loss": { - "existingJavaType": "java.util.ArrayList", - "title": "Profit/loss in dollar from the base value." - }, - "profit_loss_pct": { - "existingJavaType": "java.util.ArrayList", - "javaName": "profitLossPercent", - "title": "Profit/loss in percentage from the base value." - }, - "base_value": { - "existingJavaType": "java.lang.Double", - "title": "Basis in dollar of the profit loss calculation." - }, - "timeframe": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.portfoliohistory.enums.PortfolioTimeFrame", - "title": "Time window size of each data element." - } - } -} diff --git a/schema_json/endpoint/positions/close_position_order.json b/schema_json/endpoint/positions/close_position_order.json deleted file mode 100644 index 3ffba883..00000000 --- a/schema_json/endpoint/positions/close_position_order.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "object", - "title": "See https://alpaca.markets/docs/api-documentation/api-v2/positions/", - "properties": { - "symbol": { - "existingJavaType": "java.lang.String", - "title": "The symbol." - }, - "status": { - "existingJavaType": "java.lang.Integer", - "title": "The HTTP status code of the position-closing order." - }, - "body": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.orders.Order", - "javaName": "order", - "title": "The position-closing {@link net.jacobpeterson.alpaca.model.endpoint.orders.Order}." - } - } -} diff --git a/schema_json/endpoint/positions/position.json b/schema_json/endpoint/positions/position.json deleted file mode 100644 index 8ae71b33..00000000 --- a/schema_json/endpoint/positions/position.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "type": "object", - "title": "See Positions.", - "properties": { - "asset_id": { - "existingJavaType": "java.lang.String", - "title": "Asset ID." - }, - "symbol": { - "existingJavaType": "java.lang.String", - "title": "Symbol name of the asset." - }, - "exchange": { - "existingJavaType": "java.lang.String", - "title": "Exchange name of the asset (\"ErisX\" for crypto)." - }, - "asset_class": { - "existingJavaType": "java.lang.String", - "title": "Asset class name." - }, - "avg_entry_price": { - "existingJavaType": "java.lang.String", - "javaName": "averageEntryPrice", - "title": "Average entry price of the position." - }, - "qty": { - "existingJavaType": "java.lang.String", - "javaName": "quantity", - "title": "The number of shares." - }, - "qty_available": { - "existingJavaType": "java.lang.String", - "javaName": "quantityAvailable", - "title": "Total number of shares available minus open orders." - }, - "side": { - "existingJavaType": "java.lang.String", - "title": "\"long\"." - }, - "market_value": { - "existingJavaType": "java.lang.String", - "title": "Total dollar amount of the position." - }, - "cost_basis": { - "existingJavaType": "java.lang.String", - "title": "Total cost basis in dollar." - }, - "unrealized_pl": { - "existingJavaType": "java.lang.String", - "javaName": "unrealizedProfitLoss", - "title": "Unrealized profit/loss in dollars." - }, - "unrealized_plpc": { - "existingJavaType": "java.lang.String", - "javaName": "unrealizedProfitLossPercent", - "title": "Unrealized profit/loss percent (by a factor of 1)." - }, - "unrealized_intraday_pl": { - "existingJavaType": "java.lang.String", - "javaName": "unrealizedIntradayProfitLoss", - "title": "Unrealized profit/loss in dollars for the day." - }, - "unrealized_intraday_plpc": { - "existingJavaType": "java.lang.String", - "javaName": "unrealizedIntradayProfitLossPercent", - "title": "Unrealized profit/loss percent (by a factor of 1)." - }, - "current_price": { - "existingJavaType": "java.lang.String", - "title": "Current asset price per share." - }, - "lastday_price": { - "existingJavaType": "java.lang.String", - "title": "Last day’s asset price per share based on the closing value of the last trading day." - }, - "change_today": { - "existingJavaType": "java.lang.String", - "title": "Percent change from last day price (by a factor of 1)." - } - } -} diff --git a/schema_json/endpoint/watchlist/watchlist.json b/schema_json/endpoint/watchlist/watchlist.json deleted file mode 100644 index 7c7e8602..00000000 --- a/schema_json/endpoint/watchlist/watchlist.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "type": "object", - "title": "See Watchlist.", - "properties": { - "id": { - "existingJavaType": "java.lang.String", - "title": "Watchlist ID." - }, - "created_at": { - "existingJavaType": "java.time.ZonedDateTime", - "title": "Created at." - }, - "updated_at": { - "existingJavaType": "java.time.ZonedDateTime", - "title": "Updated at." - }, - "name": { - "existingJavaType": "java.lang.String", - "title": "User-defined watchlist name (up to 64 characters)." - }, - "account_id": { - "existingJavaType": "java.lang.String", - "title": "Account ID." - }, - "assets": { - "existingJavaType": "java.util.ArrayList", - "title": "The content of this watchlist, in the order as registered by the client." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java index 4292e85d..bcaaec25 100644 --- a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java +++ b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java @@ -1,33 +1,16 @@ package net.jacobpeterson.alpaca; -import devcsrj.okhttp3.logging.HttpLoggingInterceptor; import net.jacobpeterson.alpaca.model.properties.DataAPIType; import net.jacobpeterson.alpaca.model.properties.EndpointAPIType; import net.jacobpeterson.alpaca.properties.AlpacaProperties; -import net.jacobpeterson.alpaca.rest.AlpacaClient; -import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.account.AccountEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.accountactivities.AccountActivitiesEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.accountconfiguration.AccountConfigurationEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.assets.AssetsEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.calendar.CalendarEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.clock.ClockEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.corporateactions.CorporateActionsEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.marketdata.crypto.CryptoMarketDataEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.marketdata.stock.StockMarketDataEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.orders.OrdersEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.portfoliohistory.PortfolioHistoryEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.positions.PositionsEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.watchlist.WatchlistEndpoint; -import net.jacobpeterson.alpaca.websocket.AlpacaWebsocket; import net.jacobpeterson.alpaca.websocket.marketdata.MarketDataWebsocketInterface; import net.jacobpeterson.alpaca.websocket.marketdata.crypto.CryptoMarketDataWebsocket; -import net.jacobpeterson.alpaca.websocket.marketdata.stock.StockMarketDataWebsocket; -import net.jacobpeterson.alpaca.rest.endpoint.marketdata.news.NewsEndpoint; import net.jacobpeterson.alpaca.websocket.marketdata.news.NewsMarketDataWebsocket; +import net.jacobpeterson.alpaca.websocket.marketdata.stock.StockMarketDataWebsocket; import net.jacobpeterson.alpaca.websocket.streaming.StreamingWebsocket; import net.jacobpeterson.alpaca.websocket.streaming.StreamingWebsocketInterface; import okhttp3.OkHttpClient; +import okhttp3.logging.HttpLoggingInterceptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,45 +18,20 @@ import static com.google.common.base.Preconditions.checkNotNull; /** - * The {@link AlpacaAPI} class contains several instances of various {@link AlpacaEndpoint}s and - * {@link AlpacaWebsocket}s to interface with Alpaca. You will generally only need one instance of this class in your - * application. Note that many methods inside the various {@link AlpacaEndpoint}s allow null to be passed - * in as a parameter if it is optional. + * {@link AlpacaAPI} is the main class used to interface with the various Alpaca API endpoints. You will generally only + * need one instance of this class in your application. * - * @see Alpaca API Documentation + * @see Alpaca Docs */ public class AlpacaAPI { private static final Logger LOGGER = LoggerFactory.getLogger(AlpacaAPI.class); - private static final String VERSION_2_PATH_SEGMENT = "v2"; - private static final String VERSION_1_BETA_3_PATH_SEGMENT = "v1beta3"; - private static final String VERSION_1_BETA_1_PATH_SEGMENT = "v1beta1"; - private final OkHttpClient okHttpClient; - private final AlpacaClient brokerClient; - private final AlpacaClient cryptoDataClient; - private final AlpacaClient stockDataClient; - private final AlpacaClient newsDataClient; - // Ordering of fields/methods below are analogous to the ordering in the Alpaca documentation - private final AccountEndpoint accountEndpoint; - private final CryptoMarketDataEndpoint cryptoMarketDataEndpoint; - private final StockMarketDataEndpoint stockMarketDataEndpoint; - private final OrdersEndpoint ordersEndpoint; - private final PositionsEndpoint positionsEndpoint; - private final AssetsEndpoint assetsEndpoint; - private final WatchlistEndpoint watchlistEndpoint; - private final CalendarEndpoint calendarEndpoint; - private final ClockEndpoint clockEndpoint; - private final AccountConfigurationEndpoint accountConfigurationEndpoint; - private final AccountActivitiesEndpoint accountActivitiesEndpoint; - private final PortfolioHistoryEndpoint portfolioHistoryEndpoint; private final StreamingWebsocket streamingWebsocket; private final CryptoMarketDataWebsocket cryptoMarketDataWebsocket; private final StockMarketDataWebsocket stockMarketDataWebsocket; - private final CorporateActionsEndpoint corporateActionsEndpoint; private final NewsMarketDataWebsocket newsMarketDataWebsocket; - private final NewsEndpoint newsEndpoint; /** * Instantiates a new {@link AlpacaAPI} using properties specified in alpaca.properties file (or their @@ -154,146 +112,17 @@ public AlpacaAPI(OkHttpClient okHttpClient, String keyID, String secretKey, Stri if (okHttpClient == null) { OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder() .cache(null); // Ensure response caching is disabled - if (LOGGER.isDebugEnabled()) { - clientBuilder.addInterceptor(new HttpLoggingInterceptor(LOGGER)); + clientBuilder.addInterceptor(new HttpLoggingInterceptor(LOGGER::debug)); } - okHttpClient = clientBuilder.build(); } - this.okHttpClient = okHttpClient; - String brokerHostSubdomain; - switch (endpointAPIType) { - case LIVE: - brokerHostSubdomain = "api"; - break; - case PAPER: - brokerHostSubdomain = "paper-api"; - break; - default: - throw new UnsupportedOperationException(); - } - - if (oAuthToken == null) { - brokerClient = new AlpacaClient(okHttpClient, keyID, secretKey, - brokerHostSubdomain, VERSION_2_PATH_SEGMENT); - cryptoDataClient = new AlpacaClient(okHttpClient, keyID, secretKey, "data", VERSION_1_BETA_3_PATH_SEGMENT); - stockDataClient = new AlpacaClient(okHttpClient, keyID, secretKey, "data", VERSION_2_PATH_SEGMENT); - newsDataClient = new AlpacaClient(okHttpClient, keyID, secretKey, "data", VERSION_1_BETA_1_PATH_SEGMENT); - } else { - brokerClient = new AlpacaClient(okHttpClient, oAuthToken, brokerHostSubdomain, VERSION_2_PATH_SEGMENT); - cryptoDataClient = null; - stockDataClient = null; - newsDataClient = null; - } - - accountEndpoint = new AccountEndpoint(brokerClient); - cryptoMarketDataEndpoint = cryptoDataClient == null ? null : new CryptoMarketDataEndpoint(cryptoDataClient); - stockMarketDataEndpoint = stockDataClient == null ? null : new StockMarketDataEndpoint(stockDataClient); - ordersEndpoint = new OrdersEndpoint(brokerClient); - positionsEndpoint = new PositionsEndpoint(brokerClient); - assetsEndpoint = new AssetsEndpoint(brokerClient); - watchlistEndpoint = new WatchlistEndpoint(brokerClient); - calendarEndpoint = new CalendarEndpoint(brokerClient); - clockEndpoint = new ClockEndpoint(brokerClient); - accountConfigurationEndpoint = new AccountConfigurationEndpoint(brokerClient); - accountActivitiesEndpoint = new AccountActivitiesEndpoint(brokerClient); - portfolioHistoryEndpoint = new PortfolioHistoryEndpoint(brokerClient); - - streamingWebsocket = new StreamingWebsocket(okHttpClient, brokerHostSubdomain, keyID, secretKey, oAuthToken); - cryptoMarketDataWebsocket = cryptoDataClient == null ? null : - new CryptoMarketDataWebsocket(okHttpClient, keyID, secretKey); - stockMarketDataWebsocket = stockDataClient == null ? null : - new StockMarketDataWebsocket(okHttpClient, dataAPIType, keyID, secretKey); - corporateActionsEndpoint = new CorporateActionsEndpoint(brokerClient); - newsEndpoint = newsDataClient == null ? null : new NewsEndpoint(newsDataClient); - newsMarketDataWebsocket = newsDataClient == null ? null : new NewsMarketDataWebsocket(okHttpClient, keyID, secretKey); - } - - /** - * @return the {@link AccountEndpoint} - */ - public AccountEndpoint account() { - return accountEndpoint; - } - - /** - * @return the {@link CryptoMarketDataEndpoint} - */ - public CryptoMarketDataEndpoint cryptoMarketData() { - return cryptoMarketDataEndpoint; - } - - /** - * @return the {@link StockMarketDataEndpoint} - */ - public StockMarketDataEndpoint stockMarketData() { - return stockMarketDataEndpoint; - } - - /** - * @return the {@link OrdersEndpoint} - */ - public OrdersEndpoint orders() { - return ordersEndpoint; - } - - /** - * @return the {@link PositionsEndpoint} - */ - public PositionsEndpoint positions() { - return positionsEndpoint; - } - - /** - * @return the {@link AssetsEndpoint} - */ - public AssetsEndpoint assets() { - return assetsEndpoint; - } - - /** - * @return the {@link WatchlistEndpoint} - */ - public WatchlistEndpoint watchlist() { - return watchlistEndpoint; - } - - /** - * @return the {@link CalendarEndpoint} - */ - public CalendarEndpoint calendar() { - return calendarEndpoint; - } - - /** - * @return the {@link ClockEndpoint} - */ - public ClockEndpoint clock() { - return clockEndpoint; - } - - /** - * @return the {@link AccountConfigurationEndpoint} - */ - public AccountConfigurationEndpoint accountConfiguration() { - return accountConfigurationEndpoint; - } - - /** - * @return the {@link AccountActivitiesEndpoint} - */ - public AccountActivitiesEndpoint accountActivities() { - return accountActivitiesEndpoint; - } - - /** - * @return the {@link PortfolioHistoryEndpoint} - */ - public PortfolioHistoryEndpoint portfolioHistory() { - return portfolioHistoryEndpoint; + streamingWebsocket = new StreamingWebsocket(okHttpClient, endpointAPIType, keyID, secretKey, oAuthToken); + cryptoMarketDataWebsocket = new CryptoMarketDataWebsocket(okHttpClient, keyID, secretKey); + stockMarketDataWebsocket = new StockMarketDataWebsocket(okHttpClient, dataAPIType, keyID, secretKey); + newsMarketDataWebsocket = new NewsMarketDataWebsocket(okHttpClient, keyID, secretKey); } /** @@ -317,40 +146,6 @@ public MarketDataWebsocketInterface stockMarketDataStreaming() { return stockMarketDataWebsocket; } - public OkHttpClient getOkHttpClient() { - return okHttpClient; - } - - public AlpacaClient getBrokerClient() { - return brokerClient; - } - - public AlpacaClient getCryptoDataClient() { - return cryptoDataClient; - } - - public AlpacaClient getStockDataClient() { - return stockDataClient; - } - - public AlpacaClient getNewsDataClient() { - return newsDataClient; - } - - /** - * @return the {@link CorporateActionsEndpoint} - */ - public CorporateActionsEndpoint corporateActions() { - return corporateActionsEndpoint; - } - - /** - * @return the {@link NewsEndpoint} - */ - public NewsEndpoint newsEndpoint() { - return newsEndpoint; - } - /** * @return the News {@link MarketDataWebsocketInterface} */ @@ -358,11 +153,14 @@ public MarketDataWebsocketInterface newsMarketDataStreaming() { return newsMarketDataWebsocket; } + public OkHttpClient getOkHttpClient() { + return okHttpClient; + } /** * Creates a {@link Builder} for {@link AlpacaAPI}. * - * @return a {@link Builder} + * @return the {@link Builder} */ public static Builder builder() { return new Builder(); @@ -385,23 +183,23 @@ private Builder() { this.dataAPIType = AlpacaProperties.DATA_API_TYPE; } - public Builder withKeyID(String val) { - keyID = val; + public Builder withKeyID(String keyID) { + this.keyID = keyID; return this; } - public Builder withSecretKey(String val) { - secretKey = val; + public Builder withSecretKey(String secretKey) { + this.secretKey = secretKey; return this; } - public Builder withEndpointAPIType(EndpointAPIType val) { - endpointAPIType = val; + public Builder withEndpointAPIType(EndpointAPIType endpointAPIType) { + this.endpointAPIType = endpointAPIType; return this; } - public Builder withDataAPIType(DataAPIType val) { - dataAPIType = val; + public Builder withDataAPIType(DataAPIType dataAPIType) { + this.dataAPIType = dataAPIType; return this; } diff --git a/schema_json/endpoint/market_data/common/historical/bar/bar.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/historical/bar/bar.json similarity index 100% rename from schema_json/endpoint/market_data/common/historical/bar/bar.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/historical/bar/bar.json diff --git a/schema_json/endpoint/market_data/common/historical/bar/enums/bar_time_period.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/historical/bar/enums/bar_time_period.json similarity index 100% rename from schema_json/endpoint/market_data/common/historical/bar/enums/bar_time_period.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/historical/bar/enums/bar_time_period.json diff --git a/schema_json/endpoint/market_data/common/historical/quote/quote.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/historical/quote/quote.json similarity index 100% rename from schema_json/endpoint/market_data/common/historical/quote/quote.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/historical/quote/quote.json diff --git a/schema_json/endpoint/market_data/common/historical/trade/trade.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/historical/trade/trade.json similarity index 100% rename from schema_json/endpoint/market_data/common/historical/trade/trade.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/historical/trade/trade.json diff --git a/schema_json/endpoint/market_data/common/realtime/bar/bar_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/bar/bar_message.json similarity index 100% rename from schema_json/endpoint/market_data/common/realtime/bar/bar_message.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/bar/bar_message.json diff --git a/schema_json/endpoint/market_data/common/realtime/control/error_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/control/error_message.json similarity index 100% rename from schema_json/endpoint/market_data/common/realtime/control/error_message.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/control/error_message.json diff --git a/schema_json/endpoint/market_data/common/realtime/control/subscriptions_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/control/subscriptions_message.json similarity index 100% rename from schema_json/endpoint/market_data/common/realtime/control/subscriptions_message.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/control/subscriptions_message.json diff --git a/schema_json/endpoint/market_data/common/realtime/control/success_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/control/success_message.json similarity index 100% rename from schema_json/endpoint/market_data/common/realtime/control/success_message.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/control/success_message.json diff --git a/schema_json/endpoint/market_data/common/realtime/enums/market_data_message_type.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/enums/market_data_message_type.json similarity index 100% rename from schema_json/endpoint/market_data/common/realtime/enums/market_data_message_type.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/enums/market_data_message_type.json diff --git a/schema_json/endpoint/market_data/common/realtime/market_data_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/market_data_message.json similarity index 100% rename from schema_json/endpoint/market_data/common/realtime/market_data_message.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/market_data_message.json diff --git a/schema_json/endpoint/market_data/common/realtime/quote/quote_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/quote/quote_message.json similarity index 100% rename from schema_json/endpoint/market_data/common/realtime/quote/quote_message.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/quote/quote_message.json diff --git a/schema_json/endpoint/market_data/common/realtime/symbol_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/symbol_message.json similarity index 100% rename from schema_json/endpoint/market_data/common/realtime/symbol_message.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/symbol_message.json diff --git a/schema_json/endpoint/market_data/common/realtime/trade/trade_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/trade/trade_message.json similarity index 100% rename from schema_json/endpoint/market_data/common/realtime/trade/trade_message.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/trade/trade_message.json diff --git a/schema_json/endpoint/market_data/crypto/common/enums/taker_side.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/common/enums/taker_side.json similarity index 100% rename from schema_json/endpoint/market_data/crypto/common/enums/taker_side.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/common/enums/taker_side.json diff --git a/schema_json/endpoint/market_data/crypto/historical/bar/crypto_bar.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/bar/crypto_bar.json similarity index 100% rename from schema_json/endpoint/market_data/crypto/historical/bar/crypto_bar.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/bar/crypto_bar.json diff --git a/schema_json/endpoint/market_data/crypto/historical/bar/crypto_bars_response.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/bar/crypto_bars_response.json similarity index 100% rename from schema_json/endpoint/market_data/crypto/historical/bar/crypto_bars_response.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/bar/crypto_bars_response.json diff --git a/schema_json/endpoint/market_data/crypto/historical/bar/latest_crypto_bars_response.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/bar/latest_crypto_bars_response.json similarity index 100% rename from schema_json/endpoint/market_data/crypto/historical/bar/latest_crypto_bars_response.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/bar/latest_crypto_bars_response.json diff --git a/schema_json/endpoint/market_data/crypto/historical/orderbook/crypto_orderbook.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/orderbook/crypto_orderbook.json similarity index 100% rename from schema_json/endpoint/market_data/crypto/historical/orderbook/crypto_orderbook.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/orderbook/crypto_orderbook.json diff --git a/schema_json/endpoint/market_data/crypto/historical/orderbook/crypto_orderbook_entry.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/orderbook/crypto_orderbook_entry.json similarity index 100% rename from schema_json/endpoint/market_data/crypto/historical/orderbook/crypto_orderbook_entry.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/orderbook/crypto_orderbook_entry.json diff --git a/schema_json/endpoint/market_data/crypto/historical/orderbook/latest_crypto_orderbooks_response.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/orderbook/latest_crypto_orderbooks_response.json similarity index 100% rename from schema_json/endpoint/market_data/crypto/historical/orderbook/latest_crypto_orderbooks_response.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/orderbook/latest_crypto_orderbooks_response.json diff --git a/schema_json/endpoint/market_data/crypto/historical/quote/crypto_quote.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/quote/crypto_quote.json similarity index 100% rename from schema_json/endpoint/market_data/crypto/historical/quote/crypto_quote.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/quote/crypto_quote.json diff --git a/schema_json/endpoint/market_data/crypto/historical/quote/latest_crypto_quotes_response.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/quote/latest_crypto_quotes_response.json similarity index 100% rename from schema_json/endpoint/market_data/crypto/historical/quote/latest_crypto_quotes_response.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/quote/latest_crypto_quotes_response.json diff --git a/schema_json/endpoint/market_data/crypto/historical/snapshot/crypto_snapshot.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/snapshot/crypto_snapshot.json similarity index 100% rename from schema_json/endpoint/market_data/crypto/historical/snapshot/crypto_snapshot.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/snapshot/crypto_snapshot.json diff --git a/schema_json/endpoint/market_data/crypto/historical/snapshot/crypto_snapshots_response.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/snapshot/crypto_snapshots_response.json similarity index 100% rename from schema_json/endpoint/market_data/crypto/historical/snapshot/crypto_snapshots_response.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/snapshot/crypto_snapshots_response.json diff --git a/schema_json/endpoint/market_data/crypto/historical/trade/crypto_trade.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/trade/crypto_trade.json similarity index 100% rename from schema_json/endpoint/market_data/crypto/historical/trade/crypto_trade.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/trade/crypto_trade.json diff --git a/schema_json/endpoint/market_data/crypto/historical/trade/crypto_trades_response.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/trade/crypto_trades_response.json similarity index 100% rename from schema_json/endpoint/market_data/crypto/historical/trade/crypto_trades_response.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/trade/crypto_trades_response.json diff --git a/schema_json/endpoint/market_data/crypto/historical/trade/latest_crypto_trades_response.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/trade/latest_crypto_trades_response.json similarity index 100% rename from schema_json/endpoint/market_data/crypto/historical/trade/latest_crypto_trades_response.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/trade/latest_crypto_trades_response.json diff --git a/schema_json/endpoint/market_data/crypto/realtime/bar/crypto_bar_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/realtime/bar/crypto_bar_message.json similarity index 100% rename from schema_json/endpoint/market_data/crypto/realtime/bar/crypto_bar_message.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/realtime/bar/crypto_bar_message.json diff --git a/schema_json/endpoint/market_data/crypto/realtime/quote/crypto_quote_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/realtime/quote/crypto_quote_message.json similarity index 100% rename from schema_json/endpoint/market_data/crypto/realtime/quote/crypto_quote_message.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/realtime/quote/crypto_quote_message.json diff --git a/schema_json/endpoint/market_data/crypto/realtime/trade/crypto_trade_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/realtime/trade/crypto_trade_message.json similarity index 100% rename from schema_json/endpoint/market_data/crypto/realtime/trade/crypto_trade_message.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/realtime/trade/crypto_trade_message.json diff --git a/schema_json/endpoint/market_data/news/common/image.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/news/common/image.json similarity index 100% rename from schema_json/endpoint/market_data/news/common/image.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/news/common/image.json diff --git a/schema_json/endpoint/market_data/news/common/news_article.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/news/common/news_article.json similarity index 100% rename from schema_json/endpoint/market_data/news/common/news_article.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/news/common/news_article.json diff --git a/schema_json/endpoint/market_data/news/news_response.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/news/news_response.json similarity index 100% rename from schema_json/endpoint/market_data/news/news_response.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/news/news_response.json diff --git a/schema_json/endpoint/market_data/news/realtime/news_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/news/realtime/news_message.json similarity index 100% rename from schema_json/endpoint/market_data/news/realtime/news_message.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/news/realtime/news_message.json diff --git a/schema_json/endpoint/market_data/stock/historical/bar/enums/bar_adjustment.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/bar/enums/bar_adjustment.json similarity index 100% rename from schema_json/endpoint/market_data/stock/historical/bar/enums/bar_adjustment.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/bar/enums/bar_adjustment.json diff --git a/schema_json/endpoint/market_data/stock/historical/bar/enums/bar_feed.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/bar/enums/bar_feed.json similarity index 100% rename from schema_json/endpoint/market_data/stock/historical/bar/enums/bar_feed.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/bar/enums/bar_feed.json diff --git a/schema_json/endpoint/market_data/stock/historical/bar/multi_stock_bars_response.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/bar/multi_stock_bars_response.json similarity index 100% rename from schema_json/endpoint/market_data/stock/historical/bar/multi_stock_bars_response.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/bar/multi_stock_bars_response.json diff --git a/schema_json/endpoint/market_data/stock/historical/bar/stock_bar.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/bar/stock_bar.json similarity index 100% rename from schema_json/endpoint/market_data/stock/historical/bar/stock_bar.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/bar/stock_bar.json diff --git a/schema_json/endpoint/market_data/stock/historical/bar/stock_bars_response.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/bar/stock_bars_response.json similarity index 100% rename from schema_json/endpoint/market_data/stock/historical/bar/stock_bars_response.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/bar/stock_bars_response.json diff --git a/schema_json/endpoint/market_data/stock/historical/quote/latest_stock_quote_response.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/quote/latest_stock_quote_response.json similarity index 100% rename from schema_json/endpoint/market_data/stock/historical/quote/latest_stock_quote_response.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/quote/latest_stock_quote_response.json diff --git a/schema_json/endpoint/market_data/stock/historical/quote/stock_quote.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/quote/stock_quote.json similarity index 100% rename from schema_json/endpoint/market_data/stock/historical/quote/stock_quote.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/quote/stock_quote.json diff --git a/schema_json/endpoint/market_data/stock/historical/quote/stock_quotes_response.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/quote/stock_quotes_response.json similarity index 100% rename from schema_json/endpoint/market_data/stock/historical/quote/stock_quotes_response.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/quote/stock_quotes_response.json diff --git a/schema_json/endpoint/market_data/stock/historical/snapshot/snapshot.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/snapshot/snapshot.json similarity index 100% rename from schema_json/endpoint/market_data/stock/historical/snapshot/snapshot.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/snapshot/snapshot.json diff --git a/schema_json/endpoint/market_data/stock/historical/trade/latest_stock_trade_response.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/trade/latest_stock_trade_response.json similarity index 100% rename from schema_json/endpoint/market_data/stock/historical/trade/latest_stock_trade_response.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/trade/latest_stock_trade_response.json diff --git a/schema_json/endpoint/market_data/stock/historical/trade/stock_trade.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/trade/stock_trade.json similarity index 100% rename from schema_json/endpoint/market_data/stock/historical/trade/stock_trade.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/trade/stock_trade.json diff --git a/schema_json/endpoint/market_data/stock/historical/trade/stock_trades_response.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/trade/stock_trades_response.json similarity index 100% rename from schema_json/endpoint/market_data/stock/historical/trade/stock_trades_response.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/trade/stock_trades_response.json diff --git a/schema_json/endpoint/market_data/stock/realtime/bar/stock_bar_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/realtime/bar/stock_bar_message.json similarity index 100% rename from schema_json/endpoint/market_data/stock/realtime/bar/stock_bar_message.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/realtime/bar/stock_bar_message.json diff --git a/schema_json/endpoint/market_data/stock/realtime/quote/stock_quote_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/realtime/quote/stock_quote_message.json similarity index 100% rename from schema_json/endpoint/market_data/stock/realtime/quote/stock_quote_message.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/realtime/quote/stock_quote_message.json diff --git a/schema_json/endpoint/market_data/stock/realtime/trade/stock_trade_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/realtime/trade/stock_trade_message.json similarity index 100% rename from schema_json/endpoint/market_data/stock/realtime/trade/stock_trade_message.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/realtime/trade/stock_trade_message.json diff --git a/schema_json/endpoint/streaming/authorization/authorization_data.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/authorization/authorization_data.json similarity index 100% rename from schema_json/endpoint/streaming/authorization/authorization_data.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/authorization/authorization_data.json diff --git a/schema_json/endpoint/streaming/authorization/authorization_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/authorization/authorization_message.json similarity index 100% rename from schema_json/endpoint/streaming/authorization/authorization_message.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/authorization/authorization_message.json diff --git a/schema_json/endpoint/streaming/enums/streaming_message_type.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/enums/streaming_message_type.json similarity index 100% rename from schema_json/endpoint/streaming/enums/streaming_message_type.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/enums/streaming_message_type.json diff --git a/schema_json/endpoint/streaming/listening/listening_data.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/listening/listening_data.json similarity index 100% rename from schema_json/endpoint/streaming/listening/listening_data.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/listening/listening_data.json diff --git a/schema_json/endpoint/streaming/listening/listening_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/listening/listening_message.json similarity index 100% rename from schema_json/endpoint/streaming/listening/listening_message.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/listening/listening_message.json diff --git a/schema_json/endpoint/streaming/streaming_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/streaming_message.json similarity index 100% rename from schema_json/endpoint/streaming/streaming_message.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/streaming_message.json diff --git a/schema_json/endpoint/streaming/trade/enums/trade_update_event.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/trade/enums/trade_update_event.json similarity index 100% rename from schema_json/endpoint/streaming/trade/enums/trade_update_event.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/trade/enums/trade_update_event.json diff --git a/schema_json/endpoint/streaming/trade/trade_update.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/trade/trade_update.json similarity index 82% rename from schema_json/endpoint/streaming/trade/trade_update.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/trade/trade_update.json index 32f6c324..c0072275 100644 --- a/schema_json/endpoint/streaming/trade/trade_update.json +++ b/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/trade/trade_update.json @@ -20,8 +20,8 @@ "title": "The position quantity." }, "order": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.orders.Order", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.orders.Order}." + "existingJavaType": "net.jacobpeterson.alpaca.openapi.trader.model.Order", + "title": "The {@link net.jacobpeterson.alpaca.openapi.trader.model.Order}." } } } diff --git a/schema_json/endpoint/streaming/trade/trade_update_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/trade/trade_update_message.json similarity index 100% rename from schema_json/endpoint/streaming/trade/trade_update_message.json rename to src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/trade/trade_update_message.json diff --git a/src/main/java/net/jacobpeterson/alpaca/properties/AlpacaProperties.java b/src/main/java/net/jacobpeterson/alpaca/properties/AlpacaProperties.java index 0c121157..175a83c8 100644 --- a/src/main/java/net/jacobpeterson/alpaca/properties/AlpacaProperties.java +++ b/src/main/java/net/jacobpeterson/alpaca/properties/AlpacaProperties.java @@ -14,43 +14,16 @@ public class AlpacaProperties { private static final String ALPACA_PROPERTIES_FILE = "alpaca.properties"; private static final String ALPACA_DEFAULT_PROPERTIES_FILE = "alpaca.default.properties"; - private static final String KEY_ID_KEY = "key_id"; - /** - * The value of {@link #KEY_ID_KEY} in {@link #ALPACA_PROPERTIES_FILE}. - */ public static final String KEY_ID = getProperty( ALPACA_PROPERTIES_FILE, ALPACA_DEFAULT_PROPERTIES_FILE, - KEY_ID_KEY); - - private static final String SECRET_KEY_KEY = "secret_key"; - /** - * The value of {@link #SECRET_KEY_KEY} in {@link #ALPACA_PROPERTIES_FILE}. - */ + "key_id"); public static final String SECRET_KEY = getProperty( ALPACA_PROPERTIES_FILE, ALPACA_DEFAULT_PROPERTIES_FILE, - SECRET_KEY_KEY); - - private static final String ENDPOINT_API_TYPE_KEY = "endpoint_api_type"; - /** - * The value of {@link #ENDPOINT_API_TYPE_KEY} in {@link #ALPACA_PROPERTIES_FILE}. - */ + "secret_key"); public static final EndpointAPIType ENDPOINT_API_TYPE = EndpointAPIType.fromValue(getProperty( ALPACA_PROPERTIES_FILE, ALPACA_DEFAULT_PROPERTIES_FILE, - ENDPOINT_API_TYPE_KEY)); - - private static final String DATA_API_TYPE_KEY = "data_api_type"; - /** - * The value of {@link #DATA_API_TYPE_KEY} in {@link #ALPACA_PROPERTIES_FILE}. - */ + "endpoint_api_type")); public static final DataAPIType DATA_API_TYPE = DataAPIType.fromValue(getProperty( ALPACA_PROPERTIES_FILE, ALPACA_DEFAULT_PROPERTIES_FILE, - DATA_API_TYPE_KEY)); - - private static final String USER_AGENT_KEY = "user_agent"; - /** - * The value of {@link #USER_AGENT_KEY} in {@link #ALPACA_PROPERTIES_FILE}. - */ - public static final String USER_AGENT = getProperty( - ALPACA_PROPERTIES_FILE, ALPACA_DEFAULT_PROPERTIES_FILE, - USER_AGENT_KEY); + "data_api_type")); } diff --git a/schema_json/properties/data_api_type.json b/src/main/java/net/jacobpeterson/alpaca/properties/data_api_type.json similarity index 100% rename from schema_json/properties/data_api_type.json rename to src/main/java/net/jacobpeterson/alpaca/properties/data_api_type.json diff --git a/schema_json/properties/endpoint_api_type.json b/src/main/java/net/jacobpeterson/alpaca/properties/endpoint_api_type.json similarity index 100% rename from schema_json/properties/endpoint_api_type.json rename to src/main/java/net/jacobpeterson/alpaca/properties/endpoint_api_type.json diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClient.java b/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClient.java deleted file mode 100644 index dc11df3f..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClient.java +++ /dev/null @@ -1,235 +0,0 @@ -package net.jacobpeterson.alpaca.rest; - -import com.google.gson.Gson; -import com.google.gson.JsonElement; -import com.google.gson.JsonParser; -import net.jacobpeterson.alpaca.properties.AlpacaProperties; -import okhttp3.Headers; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.ResponseBody; - -import java.io.IOException; -import java.io.Reader; -import java.lang.reflect.Type; -import java.util.function.Predicate; - -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; -import static net.jacobpeterson.alpaca.util.gson.GsonUtil.GSON; - -/** - * {@link AlpacaClient} represents an HTTP RestAPI client for Alpaca. - */ -public class AlpacaClient { - - public static final Predicate STATUS_CODE_200_OR_207 = (code) -> code == 200 || code == 207; - public static final Predicate STATUS_CODE_200_OR_204 = (code) -> code == 200 || code == 204; - - private final OkHttpClient okHttpClient; - private final HttpUrl baseURL; - private final Headers requestHeaders; - - /** - * Instantiates a new {@link AlpacaClient}. - * - * @param okHttpClient the {@link OkHttpClient} - * @param keyID the key ID - * @param secretKey the secret key - * @param alpacaSubdomain the Alpaca subdomain - * @param versionPathSegment the version path segment e.g. "v2" - */ - public AlpacaClient(OkHttpClient okHttpClient, String keyID, String secretKey, String alpacaSubdomain, - String versionPathSegment) { - this(okHttpClient, keyID, secretKey, null, alpacaSubdomain, versionPathSegment); - } - - /** - * Instantiates a new {@link AlpacaClient}. - * - * @param okHttpClient the {@link OkHttpClient} - * @param oAuthToken the OAuth token - * @param alpacaSubdomain the Alpaca subdomain - * @param versionPathSegment the version path segment e.g. "v2" - */ - public AlpacaClient(OkHttpClient okHttpClient, String oAuthToken, String alpacaSubdomain, - String versionPathSegment) { - this(okHttpClient, null, null, oAuthToken, alpacaSubdomain, versionPathSegment); - } - - /** - * Instantiates a new {@link AlpacaClient}. - * - * @param okHttpClient the {@link OkHttpClient} - * @param keyID the key ID - * @param secretKey the secret key - * @param oAuthToken the OAuth token - * @param alpacaSubdomain the Alpaca subdomain - * @param versionPathSegment the version path segment e.g. "v2" - */ - protected AlpacaClient(OkHttpClient okHttpClient, String keyID, String secretKey, String oAuthToken, - String alpacaSubdomain, String versionPathSegment) { - checkNotNull(okHttpClient); - checkNotNull(alpacaSubdomain); - checkNotNull(versionPathSegment); - - this.okHttpClient = okHttpClient; - - baseURL = new HttpUrl.Builder() - .scheme("https") - .host(alpacaSubdomain + ".alpaca.markets") - .addPathSegment(versionPathSegment) - .build(); - - Headers.Builder requestHeadersBuilder = new Headers.Builder(); - requestHeadersBuilder.add("User-Agent", AlpacaProperties.USER_AGENT); - if (oAuthToken != null) { - requestHeadersBuilder.add("Authorization", "Bearer " + oAuthToken); - } else { - requestHeadersBuilder - .add("APCA-API-KEY-ID", keyID) - .add("APCA-API-SECRET-KEY", secretKey); - } - requestHeaders = requestHeadersBuilder.build(); - } - - /** - * Gets a new {@link okhttp3.HttpUrl.Builder}. - * - * @return a {@link okhttp3.HttpUrl.Builder} - */ - public HttpUrl.Builder urlBuilder() { - return baseURL.newBuilder(); - } - - /** - * Gets a new {@link okhttp3.Request.Builder} with {@link #requestHeaders}. - * - * @param httpUrl the {@link okhttp3.HttpUrl} - * - * @return a {@link okhttp3.Request.Builder} - */ - public Request.Builder requestBuilder(HttpUrl httpUrl) { - return new Request.Builder().headers(requestHeaders).url(httpUrl); - } - - /** - * Calls {@link #requestObject(Request, Predicate, Type)} with a success code of 200. - */ - public T requestObject(Request request, Type type) throws AlpacaClientException { - return requestObject(request, (code) -> code == 200, type); - } - - /** - * Requests an object given a {@link Request} that is deserialized via {@link Gson#fromJson(Reader, Type)}. - * - * @param the type of object - * @param request the {@link Request} - * @param isSuccessCode throws {@link AlpacaClientException} if passed in {@link Response#code()} to the - * {@link Predicate#test(Object)} returns false - * @param type the object {@link Type} - * - * @return the requested object - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public T requestObject(Request request, Predicate isSuccessCode, Type type) - throws AlpacaClientException { - return GSON.fromJson(requestJSON(request, isSuccessCode), type); - } - - /** - * Calls {@link #requestJSON(Request, Predicate)} with a success code of 200. - */ - public JsonElement requestJSON(Request request) throws AlpacaClientException { - return requestJSON(request, (code) -> code == 200); - } - - /** - * Requests a {@link JsonElement} given a {@link Request}. - * - * @param request the {@link Request} - * @param isSuccessCode throws {@link AlpacaClientException} if passed in {@link Response#code()} to the - * {@link Predicate#test(Object)} returns false - * - * @return the {@link JsonElement} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public JsonElement requestJSON(Request request, Predicate isSuccessCode) throws AlpacaClientException { - try (Response response = request(request, isSuccessCode)) { - try (ResponseBody responseBody = response.body()) { - // Throw exception if 'responseBody' is ever null since we never want to use cachedResponse or - // other types of responses. - checkState(responseBody != null); - - try (Reader charStream = responseBody.charStream()) { - return JsonParser.parseReader(charStream); - } catch (IOException ioException) { - throw new AlpacaClientException(ioException); - } - } - } - } - - /** - * Sends a {@link Request} and ignores any {@link ResponseBody}. - * - * @param request the {@link Request} - * @param isSuccessCode throws {@link AlpacaClientException} if passed in {@link Response#code()} to the - * {@link Predicate#test(Object)} returns false - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public void requestVoid(Request request, Predicate isSuccessCode) throws AlpacaClientException { - // close() will throw an exception if cachedResponse or others were used which is fine - // since we never want to use those types of responses. - request(request, isSuccessCode).close(); - } - - /** - * Sends a {@link Request} and returns a {@link Response}. - * - * @param request the {@link Request} - * @param isSuccessCode throws {@link AlpacaClientException} if passed in {@link Response#code()} to the - * {@link Predicate#test(Object)} returns false - * - * @return the {@link Response}. Be sure to call {@link Response#close()} after use. - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Response request(Request request, Predicate isSuccessCode) throws AlpacaClientException { - try { - Response response = executeRequest(request); - if (!isSuccessCode.test(response.code())) { - throw new AlpacaClientException(response); - } - return response; - } catch (IOException ioException) { - throw new AlpacaClientException(ioException); - } - } - - /** - * Executes a {@link Request}. - * - * @param request the {@link Request} - * - * @return the {@link Response}. Be sure to call {@link Response#close()} after use. - * - * @throws IOException thrown for {@link IOException}s - */ - public Response executeRequest(Request request) throws IOException { - return okHttpClient.newCall(request).execute(); - } - - public HttpUrl getBaseURL() { - return baseURL; - } - - public Headers getRequestHeaders() { - return requestHeaders; - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClientException.java b/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClientException.java deleted file mode 100644 index 4c902b05..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClientException.java +++ /dev/null @@ -1,144 +0,0 @@ -package net.jacobpeterson.alpaca.rest; - -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import okhttp3.Response; -import okhttp3.ResponseBody; - -import javax.annotation.Nullable; - -public class AlpacaClientException extends Exception { - - private static final String CODE_KEY = "code"; - private static final String MESSAGE_KEY = "message"; - - private String responseBody; - private Integer responseStatusCode; - private String responseStatusMessage; - private Integer apiResponseCode; - private String apiResponseMessage; - - /** - * Instantiates a new {@link AlpacaClientException}. - * - * @param message the message - */ - public AlpacaClientException(String message) { - super(message); - } - - /** - * Instantiates a new {@link AlpacaClientException}. - * - * @param cause the cause {@link Throwable} - */ - public AlpacaClientException(Throwable cause) { - super(cause); - } - - /** - * Instantiates a new {@link AlpacaClientException}. - * - * @param message the message - * @param cause the cause {@link Throwable} - */ - public AlpacaClientException(String message, Throwable cause) { - super(message, cause); - } - - /** - * Instantiates a new {@link AlpacaClientException}. - * - * @param response the {@link Response} containing an API response code and message JSON - */ - protected AlpacaClientException(Response response) { - responseStatusCode = response.code(); - responseStatusMessage = response.message(); - - try (ResponseBody clientResponseBody = response.body()) { - if (clientResponseBody != null) { - this.responseBody = clientResponseBody.string(); - } - } catch (Exception ignored) {} - } - - @Override - public String getMessage() { - if (responseBody == null) { - return super.getMessage(); - } else { - return parseAPIErrorResponse(); - } - } - - /** - * Parses an API error response of the following format: - *
-     *  {
-     *     "code": 505,
-     *     "message": "Error message"
-     *  }
-     * 
- * and sets {@link #apiResponseCode} and {@link #apiResponseMessage} accordingly. - * - * @return a formatted message of the error response - */ - private String parseAPIErrorResponse() { - try { - JsonElement responseJsonElement = JsonParser.parseString(responseBody); - - if (responseJsonElement instanceof JsonObject) { - JsonObject responseJsonObject = responseJsonElement.getAsJsonObject(); - - if (responseJsonObject.has(CODE_KEY)) { - apiResponseCode = responseJsonObject.get(CODE_KEY).getAsInt(); - } - - if (responseJsonObject.has(MESSAGE_KEY)) { - apiResponseMessage = responseJsonObject.get(MESSAGE_KEY).getAsString(); - } - } - - // Just use the response JSON if the message could not be parsed. - if (apiResponseMessage == null) { - apiResponseMessage = responseJsonElement.toString(); - } - } catch (Exception ignored) {} - - // Build message - StringBuilder messageBuilder = new StringBuilder(); - messageBuilder.append("Status Code: ").append(responseStatusCode); - messageBuilder.append(", Status Message: \"").append(responseStatusMessage).append("\""); - - if (apiResponseCode != null) { - messageBuilder.append(", API Response Code: ").append(apiResponseCode); - } - - if (apiResponseMessage != null) { - messageBuilder.append(", API Response Message: \"").append(apiResponseMessage).append("\""); - } - - return messageBuilder.toString(); - } - - @Nullable - public Integer getResponseStatusCode() { - return responseStatusCode; - } - - @Nullable - public String getResponseStatusMessage() { - return responseStatusMessage; - } - - @Nullable - public Integer getAPIResponseCode() { - return apiResponseCode; - } - - @Nullable - public String getAPIResponseMessage() { - return apiResponseMessage; - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AlpacaEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AlpacaEndpoint.java deleted file mode 100644 index dc4e1616..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AlpacaEndpoint.java +++ /dev/null @@ -1,23 +0,0 @@ -package net.jacobpeterson.alpaca.rest.endpoint; - -import net.jacobpeterson.alpaca.rest.AlpacaClient; - -/** - * {@link AlpacaEndpoint} is an abstract class representing a RestAPI endpoint. - */ -public abstract class AlpacaEndpoint { - - protected final AlpacaClient alpacaClient; - protected final String endpointPathSegment; - - /** - * Instantiates a new {@link AlpacaEndpoint}. - * - * @param alpacaClient the {@link AlpacaClient} - * @param endpointPathSegment the endpoint path segment relative to {@link AlpacaClient#getBaseURL()} - */ - public AlpacaEndpoint(AlpacaClient alpacaClient, String endpointPathSegment) { - this.alpacaClient = alpacaClient; - this.endpointPathSegment = endpointPathSegment; - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/account/AccountEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/account/AccountEndpoint.java deleted file mode 100644 index 8054ab4f..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/account/AccountEndpoint.java +++ /dev/null @@ -1,39 +0,0 @@ -package net.jacobpeterson.alpaca.rest.endpoint.account; - -import net.jacobpeterson.alpaca.model.endpoint.account.Account; -import net.jacobpeterson.alpaca.rest.AlpacaClient; -import net.jacobpeterson.alpaca.rest.AlpacaClientException; -import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; -import okhttp3.HttpUrl; -import okhttp3.Request; - -/** - * {@link AlpacaEndpoint} for Account. - */ -public class AccountEndpoint extends AlpacaEndpoint { - - /** - * Instantiates a new {@link AccountEndpoint}. - * - * @param alpacaClient the {@link AlpacaClient} - */ - public AccountEndpoint(AlpacaClient alpacaClient) { - super(alpacaClient, "account"); - } - - /** - * Returns the {@link Account}. - * - * @return the {@link Account} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Account get() throws AlpacaClientException { - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment); - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, Account.class); - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/accountactivities/AccountActivitiesEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/accountactivities/AccountActivitiesEndpoint.java deleted file mode 100644 index c56375bf..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/accountactivities/AccountActivitiesEndpoint.java +++ /dev/null @@ -1,142 +0,0 @@ -package net.jacobpeterson.alpaca.rest.endpoint.accountactivities; - -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import net.jacobpeterson.alpaca.model.endpoint.accountactivities.AccountActivity; -import net.jacobpeterson.alpaca.model.endpoint.accountactivities.NonTradeActivity; -import net.jacobpeterson.alpaca.model.endpoint.accountactivities.TradeActivity; -import net.jacobpeterson.alpaca.model.endpoint.accountactivities.enums.ActivityType; -import net.jacobpeterson.alpaca.model.endpoint.common.enums.SortDirection; -import net.jacobpeterson.alpaca.rest.AlpacaClient; -import net.jacobpeterson.alpaca.rest.AlpacaClientException; -import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; -import okhttp3.HttpUrl; -import okhttp3.Request; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -import static com.google.common.base.Preconditions.checkState; -import static net.jacobpeterson.alpaca.util.gson.GsonUtil.GSON; - -/** - * {@link AlpacaEndpoint} for - * Account - * Activities. - */ -public class AccountActivitiesEndpoint extends AlpacaEndpoint { - - private static final Logger LOGGER = LoggerFactory.getLogger(AccountActivitiesEndpoint.class); - - private static final String ACTIVITY_TYPE_FIELD = "activity_type"; - - /** - * Instantiates a new {@link AccountActivitiesEndpoint}. - * - * @param alpacaClient the {@link AlpacaClient} - */ - public AccountActivitiesEndpoint(AlpacaClient alpacaClient) { - super(alpacaClient, "account/activities"); - } - - /** - * Returns {@link AccountActivity} entries for many {@link ActivityType}s or for a specific {@link ActivityType}. - * - * @param date the date for which you want to see activities. - * @param until the response will contain only activities submitted before this date. (Cannot be used with - * date.) - * @param after the response will contain only activities submitted after this date. (Cannot be used with - * date.) - * @param sortDirection the {@link SortDirection} (defaults to {@link SortDirection#DESCENDING} if unspecified.) - * @param pageSize the maximum number of entries to return in the response. (See the section on paging.) - * @param pageToken the ID of the end of your current page of results. (See the section on paging.) - * @param activityTypes the {@link ActivityType}s (null for all {@link ActivityType}s) - * - * @return a {@link List} of {@link AccountActivity}s - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public List get(ZonedDateTime date, ZonedDateTime until, ZonedDateTime after, - SortDirection sortDirection, Integer pageSize, String pageToken, ActivityType... activityTypes) - throws AlpacaClientException { - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegments(endpointPathSegment); - - if (activityTypes != null) { // Check if we don't want all activity types - if (activityTypes.length == 1) { // Get one activity - urlBuilder.addPathSegment(activityTypes[0].toString()); - } else { // Get list of activities - urlBuilder.addQueryParameter("activity_types", - // Makes comma-separated list - Arrays.stream(activityTypes).map(ActivityType::toString).collect(Collectors.joining(","))); - } - } - - if (date != null) { - urlBuilder.addQueryParameter("date", date.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); - } - - if (until != null) { - urlBuilder.addQueryParameter("until", until.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); - } - - if (after != null) { - urlBuilder.addQueryParameter("after", after.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); - } - - if (sortDirection != null) { - urlBuilder.addQueryParameter("direction", sortDirection.toString()); - } - - if (pageSize != null) { - urlBuilder.addQueryParameter("page_size", pageSize.toString()); - } - - if (pageToken != null) { - urlBuilder.addQueryParameter("page_token", pageToken); - } - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - - JsonElement responseJSON = alpacaClient.requestJSON(request); - checkState(responseJSON instanceof JsonArray, "The response must be an array! Received: %s", responseJSON); - - ArrayList accountActivities = new ArrayList<>(); - for (JsonElement responseArrayElement : responseJSON.getAsJsonArray()) { - checkState(responseArrayElement instanceof JsonObject, - "All array elements must be objects! Received: %s", responseArrayElement); - - JsonObject responseArrayObject = responseArrayElement.getAsJsonObject(); - JsonElement activityTypeElement = responseArrayObject.get(ACTIVITY_TYPE_FIELD); - checkState(activityTypeElement != null, "Activity type elements must have %s field! Received: %s", - ACTIVITY_TYPE_FIELD, responseArrayElement); - - String activityType = activityTypeElement.getAsString(); - AccountActivity accountActivity; - - // A 'TradeActivity' always has 'activity_type' field as 'FILL' - if (activityType.equals(ActivityType.FILL.toString())) { - accountActivity = GSON.fromJson(responseArrayObject, TradeActivity.class); - } else { - accountActivity = GSON.fromJson(responseArrayObject, NonTradeActivity.class); - } - - if (accountActivity.getActivityType() == null) { - LOGGER.error("No ActivityType enum exists for {}. Report this!", activityType); - } - - accountActivities.add(accountActivity); - } - - return accountActivities; - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/accountconfiguration/AccountConfigurationEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/accountconfiguration/AccountConfigurationEndpoint.java deleted file mode 100644 index a319e650..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/accountconfiguration/AccountConfigurationEndpoint.java +++ /dev/null @@ -1,64 +0,0 @@ -package net.jacobpeterson.alpaca.rest.endpoint.accountconfiguration; - -import net.jacobpeterson.alpaca.model.endpoint.accountconfiguration.AccountConfiguration; -import net.jacobpeterson.alpaca.rest.AlpacaClient; -import net.jacobpeterson.alpaca.rest.AlpacaClientException; -import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; -import net.jacobpeterson.alpaca.util.okhttp.JSONBodyBuilder; -import okhttp3.HttpUrl; -import okhttp3.Request; - -import static com.google.common.base.Preconditions.checkNotNull; -import static net.jacobpeterson.alpaca.util.gson.GsonUtil.GSON; - -/** - * {@link AlpacaEndpoint} for - * Account Configuration. - */ -public class AccountConfigurationEndpoint extends AlpacaEndpoint { - - /** - * Instantiates a new {@link AccountConfigurationEndpoint}. - * - * @param alpacaClient the {@link AlpacaClient} - */ - public AccountConfigurationEndpoint(AlpacaClient alpacaClient) { - super(alpacaClient, "account/configurations"); - } - - /** - * Returns the {@link AccountConfiguration}. - * - * @return the {@link AccountConfiguration} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public AccountConfiguration get() throws AlpacaClientException { - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegments(endpointPathSegment); - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, AccountConfiguration.class); - } - - /** - * Sets the {@link AccountConfiguration}. - * - * @param accountConfiguration the {@link AccountConfiguration} - * - * @return the updated {@link AccountConfiguration} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public AccountConfiguration set(AccountConfiguration accountConfiguration) throws AlpacaClientException { - checkNotNull(accountConfiguration); - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegments(endpointPathSegment); - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .patch(new JSONBodyBuilder(GSON.toJson(accountConfiguration)).build()) - .build(); - return alpacaClient.requestObject(request, AccountConfiguration.class); - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/assets/AssetsEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/assets/AssetsEndpoint.java deleted file mode 100644 index 82763589..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/assets/AssetsEndpoint.java +++ /dev/null @@ -1,83 +0,0 @@ -package net.jacobpeterson.alpaca.rest.endpoint.assets; - -import com.google.gson.reflect.TypeToken; -import net.jacobpeterson.alpaca.model.endpoint.assets.Asset; -import net.jacobpeterson.alpaca.model.endpoint.assets.enums.AssetClass; -import net.jacobpeterson.alpaca.model.endpoint.assets.enums.AssetStatus; -import net.jacobpeterson.alpaca.rest.AlpacaClient; -import net.jacobpeterson.alpaca.rest.AlpacaClientException; -import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; -import okhttp3.HttpUrl; -import okhttp3.Request; - -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.List; - -import static com.google.common.base.Preconditions.checkNotNull; - -/** - * {@link AlpacaEndpoint} for Assets. - */ -public class AssetsEndpoint extends AlpacaEndpoint { - - private static final Type ASSET_ARRAYLIST_TYPE = new TypeToken>() {}.getType(); - - /** - * Instantiates a new {@link AssetsEndpoint}. - * - * @param alpacaClient the {@link AlpacaClient} - */ - public AssetsEndpoint(AlpacaClient alpacaClient) { - super(alpacaClient, "assets"); - } - - /** - * Get a list of {@link Asset}s. - * - * @param assetStatus the {@link AssetStatus}. By default, all {@link AssetStatus}es are included. - * @param assetClass the {@link AssetClass} (null for {@link AssetClass#US_EQUITY}) - * - * @return a {@link List} of {@link Asset}s - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public List get(AssetStatus assetStatus, AssetClass assetClass) throws AlpacaClientException { - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment); - - if (assetStatus != null) { - urlBuilder.addQueryParameter("status", assetStatus.toString()); - } - - if (assetClass != null) { - urlBuilder.addQueryParameter("asset_class", assetClass.toString()); - } - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, ASSET_ARRAYLIST_TYPE); - } - - /** - * Gets an {@link Asset} by a symbol or {@link Asset#getId()}. - * - * @param symbolOrAssetID the symbol or {@link Asset#getId()} - * - * @return the {@link Asset} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Asset getBySymbol(String symbolOrAssetID) throws AlpacaClientException { - checkNotNull(symbolOrAssetID); - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment) - .addPathSegment(symbolOrAssetID); - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, Asset.class); - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/calendar/CalendarEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/calendar/CalendarEndpoint.java deleted file mode 100644 index cc1cb5d2..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/calendar/CalendarEndpoint.java +++ /dev/null @@ -1,71 +0,0 @@ -package net.jacobpeterson.alpaca.rest.endpoint.calendar; - -import com.google.gson.reflect.TypeToken; -import net.jacobpeterson.alpaca.model.endpoint.calendar.Calendar; -import net.jacobpeterson.alpaca.rest.AlpacaClient; -import net.jacobpeterson.alpaca.rest.AlpacaClientException; -import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; -import okhttp3.HttpUrl; -import okhttp3.Request; - -import java.lang.reflect.Type; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.List; - -/** - * {@link AlpacaEndpoint} for Calendar. - */ -public class CalendarEndpoint extends AlpacaEndpoint { - - private static final Type CALENDAR_ARRAYLIST_TYPE = new TypeToken>() {}.getType(); - - /** - * Instantiates a new {@link CalendarEndpoint}. - * - * @param alpacaClient the {@link AlpacaClient} - */ - public CalendarEndpoint(AlpacaClient alpacaClient) { - super(alpacaClient, "calendar"); - } - - /** - * Returns the market {@link Calendar} from 1970 to 2029. - * - * @return a {@link List} of {@link Calendar}s - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public List get() throws AlpacaClientException { - return get(null, null); - } - - /** - * Returns the market {@link Calendar}. - * - * @param start the first date to retrieve data for (inclusive) - * @param end the last date to retrieve data for (inclusive) - * - * @return a {@link List} of {@link Calendar}s - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public List get(LocalDate start, LocalDate end) throws AlpacaClientException { - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment); - - if (start != null) { - urlBuilder.addQueryParameter("start", start.format(DateTimeFormatter.ISO_DATE)); - } - - if (end != null) { - urlBuilder.addQueryParameter("end", end.format(DateTimeFormatter.ISO_DATE)); - } - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, CALENDAR_ARRAYLIST_TYPE); - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/clock/ClockEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/clock/ClockEndpoint.java deleted file mode 100644 index 368edc13..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/clock/ClockEndpoint.java +++ /dev/null @@ -1,39 +0,0 @@ -package net.jacobpeterson.alpaca.rest.endpoint.clock; - -import net.jacobpeterson.alpaca.model.endpoint.clock.Clock; -import net.jacobpeterson.alpaca.rest.AlpacaClient; -import net.jacobpeterson.alpaca.rest.AlpacaClientException; -import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; -import okhttp3.HttpUrl; -import okhttp3.Request; - -/** - * {@link AlpacaEndpoint} for Clock. - */ -public class ClockEndpoint extends AlpacaEndpoint { - - /** - * Instantiates a new {@link ClockEndpoint}. - * - * @param alpacaClient the {@link AlpacaClient} - */ - public ClockEndpoint(AlpacaClient alpacaClient) { - super(alpacaClient, "clock"); - } - - /** - * Returns the market {@link Clock}. - * - * @return the market {@link Clock} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Clock get() throws AlpacaClientException { - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment); - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, Clock.class); - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/corporateactions/CorporateActionsEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/corporateactions/CorporateActionsEndpoint.java deleted file mode 100644 index f467c4f0..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/corporateactions/CorporateActionsEndpoint.java +++ /dev/null @@ -1,121 +0,0 @@ -package net.jacobpeterson.alpaca.rest.endpoint.corporateactions; - -import com.google.gson.reflect.TypeToken; -import net.jacobpeterson.alpaca.model.endpoint.corporateactions.common.Announcement; -import net.jacobpeterson.alpaca.rest.AlpacaClient; -import net.jacobpeterson.alpaca.rest.AlpacaClientException; -import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; -import okhttp3.HttpUrl; -import okhttp3.Request; - -import java.lang.reflect.Type; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import static com.google.common.base.Preconditions.checkNotNull; - -/** - * {@link AlpacaEndpoint} for - * Corporate actions - * Endpoint. - */ -public class CorporateActionsEndpoint extends AlpacaEndpoint { - - private static final Type ANNOUNCEMENT_ARRAYLIST_TYPE = new TypeToken>() {}.getType(); - - private static final List ALL_CA_TYPES = Arrays.asList("dividend", "merger", "spinoff", "split"); - - /** - * Instantiates a new {@link AlpacaEndpoint}. - * - * @param alpacaClient the {@link AlpacaClient} - */ - public CorporateActionsEndpoint(AlpacaClient alpacaClient) { - super(alpacaClient, "corporate_actions/announcements"); - } - - /** - * Gets a {@link Announcement} of the requested id. - * @param id - announcement id to query for - * @return the {@link Announcement} - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Announcement getAnnouncement(String id) throws AlpacaClientException { - checkNotNull(id); - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment) - .addPathSegment(id); - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, Announcement.class); - - } - - /** - * Gets list of all {@link Announcement} within last 90 days - * @return a {@link List} of {@link Announcement} - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public List getAnnouncements() throws AlpacaClientException { - return this.getAnnouncements(ALL_CA_TYPES, LocalDate.now().minusDays(90), LocalDate.now(), null, null, null); - } - - /** - * Gets list of all {@link Announcement} within last 90 days for specific symbol - * @param symbol the symbol to query for - * @return a {@link List} of {@link Announcement} - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public List getAnnouncements(String symbol) throws AlpacaClientException { - return this.getAnnouncements(ALL_CA_TYPES, LocalDate.now().minusDays(90), LocalDate.now(), symbol, null, null); - } - - /** - * Gets list of all {@link Announcement} for specific query - * @param caTypes - a list of Dividend, Merger, Spinoff, or Split - * @param since - the start (inclusive) of the date range when searching corporate action announcements. This should follow the YYYY-MM-DD format. The date range is limited to 90 days - * @param until - the end (inclusive) of the date range when searching corporate action announcements. This should follow the YYYY-MM-DD format. The date range is limited to 90 days - * @param symbol - the symbol of the company initiating the announcement - * @param cusip - the CUSIP of the company initiating the announcement - * @param dateType - one of declaration_date, ex_date, record_date, or payable_date - * @return a {@link List} of {@link Announcement} - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public List getAnnouncements( - List caTypes, LocalDate since, LocalDate until, - String symbol, - String cusip, - String dateType - ) - throws AlpacaClientException { - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment); - checkNotNull(caTypes); - checkNotNull(since); - checkNotNull(until); - - - urlBuilder.addQueryParameter("ca_types", String.join(",", caTypes)); - - urlBuilder.addQueryParameter("since", since.toString()); - urlBuilder.addQueryParameter("until", until.toString()); - - if (symbol != null) { - urlBuilder.addQueryParameter("symbol", symbol); - } - if (cusip != null) { - urlBuilder.addQueryParameter("cusip", cusip); - } - if (dateType !=null) { - urlBuilder.addQueryParameter("date_type", dateType); - } - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, ANNOUNCEMENT_ARRAYLIST_TYPE); - - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/crypto/CryptoMarketDataEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/crypto/CryptoMarketDataEndpoint.java deleted file mode 100644 index f429a32c..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/crypto/CryptoMarketDataEndpoint.java +++ /dev/null @@ -1,302 +0,0 @@ -package net.jacobpeterson.alpaca.rest.endpoint.marketdata.crypto; - -import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.historical.bar.enums.BarTimePeriod; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.bar.CryptoBar; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.bar.CryptoBarsResponse; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.bar.LatestCryptoBarsResponse; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.orderbook.CryptoOrderbook; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.orderbook.LatestCryptoOrderbooksResponse; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.CryptoQuote; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.LatestCryptoQuotesResponse; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.snapshot.CryptoSnapshot; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.snapshot.CryptoSnapshotsResponse; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.CryptoTrade; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.CryptoTradesResponse; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.LatestCryptoTradesResponse; -import net.jacobpeterson.alpaca.rest.AlpacaClient; -import net.jacobpeterson.alpaca.rest.AlpacaClientException; -import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; -import net.jacobpeterson.alpaca.util.format.FormatUtil; -import okhttp3.HttpUrl; -import okhttp3.Request; - -import java.time.ZonedDateTime; -import java.util.Collection; - -import static com.google.common.base.Preconditions.checkNotNull; - -/** - * {@link AlpacaEndpoint} for - * Historical - * Crypto Market Data API. - */ -public class CryptoMarketDataEndpoint extends AlpacaEndpoint { - - public static final String LOCALE = "us"; - - /** - * Instantiates a new {@link CryptoMarketDataEndpoint}. - * - * @param alpacaClient the {@link AlpacaClient} - */ - public CryptoMarketDataEndpoint(AlpacaClient alpacaClient) { - super(alpacaClient, "crypto"); - } - - /** - * Gets {@link CryptoTrade} historical data for the requested crypto symbols. - * - * @param symbols a {@link Collection} of symbols to query for - * @param start filter data equal to or after this {@link ZonedDateTime}. Fractions of a second are not - * accepted. null for the current day in Central Time. - * @param end filter data equal to or before this {@link ZonedDateTime}. Fractions of a second are not - * accepted. null for now. - * @param limit number of data points to return. Must be in range 1-10000, defaults to 1000 if null - * is given - * @param pageToken pagination token to continue from - * - * @return the {@link CryptoTradesResponse} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public CryptoTradesResponse getTrades(Collection symbols, ZonedDateTime start, - ZonedDateTime end, Integer limit, String pageToken) throws AlpacaClientException { - checkNotNull(symbols); - if (symbols.isEmpty()) { - throw new AlpacaClientException("Collection symbols must not be empty."); - } - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment) - .addPathSegment(LOCALE) - .addPathSegment("trades"); - - urlBuilder.addQueryParameter("symbols", String.join(",", symbols)); - - if (start != null) { - urlBuilder.addQueryParameter("start", FormatUtil.toRFC3339Format(start)); - } - - if (end != null) { - urlBuilder.addQueryParameter("end", FormatUtil.toRFC3339Format(end)); - } - - if (limit != null) { - urlBuilder.addQueryParameter("limit", limit.toString()); - } - - if (pageToken != null) { - urlBuilder.addQueryParameter("page_token", pageToken); - } - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, CryptoTradesResponse.class); - } - - /** - * Gets the latest {@link CryptoTrade}s for the requested securities. - * - * @param symbols a {@link Collection} of symbols to query for - * - * @return the {@link LatestCryptoTradesResponse} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public LatestCryptoTradesResponse getLatestTrades(Collection symbols) throws AlpacaClientException { - checkNotNull(symbols); - if (symbols.isEmpty()) { - throw new AlpacaClientException("Collection symbols must not be empty."); - } - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment) - .addPathSegment(LOCALE) - .addPathSegment("latest") - .addPathSegment("trades"); - - urlBuilder.addQueryParameter("symbols", String.join(",", symbols)); - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, LatestCryptoTradesResponse.class); - } - - /** - * Gets the latest {@link CryptoBar}s for the requested securities. - * - * @param symbols a {@link Collection} of symbols to query for - * - * @return the {@link LatestCryptoBarsResponse} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public LatestCryptoBarsResponse getLatestBars(Collection symbols) throws AlpacaClientException { - checkNotNull(symbols); - if (symbols.isEmpty()) { - throw new AlpacaClientException("Collection symbols must not be empty."); - } - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment) - .addPathSegment(LOCALE) - .addPathSegment("latest") - .addPathSegment("bars"); - - urlBuilder.addQueryParameter("symbols", String.join(",", symbols)); - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, LatestCryptoBarsResponse.class); - } - - /** - * Gets the latest {@link CryptoQuote}s for the requested securities. - * - * @param symbols a {@link Collection} of symbols to query for - * - * @return the {@link LatestCryptoQuotesResponse} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public LatestCryptoQuotesResponse getLatestQuotes(Collection symbols) throws AlpacaClientException { - checkNotNull(symbols); - if (symbols.isEmpty()) { - throw new AlpacaClientException("Collection symbols must not be empty."); - } - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment) - .addPathSegment(LOCALE) - .addPathSegment("latest") - .addPathSegment("quotes"); - - urlBuilder.addQueryParameter("symbols", String.join(",", symbols)); - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, LatestCryptoQuotesResponse.class); - } - - /** - * Gets the latest {@link CryptoSnapshot} for the requested securities. - * - * @param symbols a {@link Collection} of symbols to query for - * - * @return the {@link CryptoSnapshotsResponse} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public CryptoSnapshotsResponse getSnapshots(Collection symbols) throws AlpacaClientException { - checkNotNull(symbols); - if (symbols.isEmpty()) { - throw new AlpacaClientException("Collection symbols must not be empty."); - } - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment) - .addPathSegment(LOCALE) - .addPathSegment("snapshots"); - - urlBuilder.addQueryParameter("symbols", String.join(",", symbols)); - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, CryptoSnapshotsResponse.class); - } - - /** - * Gets the latest {@link CryptoOrderbook}s for the requested securities. - * - * @param symbols a {@link Collection} of symbols to query for - * - * @return the {@link LatestCryptoOrderbooksResponse} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public LatestCryptoOrderbooksResponse getLatestOrderbooks(Collection symbols) throws AlpacaClientException { - checkNotNull(symbols); - if (symbols.isEmpty()) { - throw new AlpacaClientException("Collection symbols must not be empty."); - } - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment) - .addPathSegment(LOCALE) - .addPathSegment("latest") - .addPathSegment("orderbooks"); - - urlBuilder.addQueryParameter("symbols", String.join(",", symbols)); - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, LatestCryptoOrderbooksResponse.class); - } - - /** - * Gets {@link CryptoBar} aggregate historical data for the requested crypto. - * - * @param symbols a {@link Collection} of symbols to query for - * @param start filter data equal to or after this {@link ZonedDateTime}. Fractions of a second are - * not accepted. null for now. - * @param end filter data equal to or before this {@link ZonedDateTime}. Fractions of a second are - * not accepted. null for now. - * @param limit number of data points to return. Must be in range 1-10000, defaults to 1000 if - * null is given - * @param pageToken pagination token to continue from - * @param barTimePeriodDuration the duration for the given barTimePeriod parameter. e.g. for - * 15Min bars, you would supply 15 for this parameter and - * {@link BarTimePeriod#MINUTE} for the barTimePeriod parameter. - * @param barTimePeriod the {@link BarTimePeriod} e.g. for 15Min bars, you would supply - * {@link BarTimePeriod#MINUTE} for this parameter and 15 for the - * barTimePeriodDuration parameter. - * - * @return the {@link CryptoBarsResponse} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public CryptoBarsResponse getBars(Collection symbols, ZonedDateTime start, ZonedDateTime end, Integer limit, - String pageToken, int barTimePeriodDuration, BarTimePeriod barTimePeriod) throws AlpacaClientException { - checkNotNull(symbols); - if (symbols.isEmpty()) { - throw new AlpacaClientException("Collection symbols must not be empty."); - } - checkNotNull(barTimePeriod); - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment) - .addPathSegment(LOCALE) - .addPathSegment("bars"); - - urlBuilder.addQueryParameter("symbols", String.join(",", symbols)); - - if (start != null) { - urlBuilder.addQueryParameter("start", FormatUtil.toRFC3339Format(start)); - } - - if (end != null) { - urlBuilder.addQueryParameter("end", FormatUtil.toRFC3339Format(end)); - } - - if (limit != null) { - urlBuilder.addQueryParameter("limit", limit.toString()); - } - - if (pageToken != null) { - urlBuilder.addQueryParameter("page_token", pageToken); - } - - urlBuilder.addQueryParameter("timeframe", barTimePeriodDuration + barTimePeriod.toString()); - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, CryptoBarsResponse.class); - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/news/NewsEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/news/NewsEndpoint.java deleted file mode 100644 index 88ee1b7c..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/news/NewsEndpoint.java +++ /dev/null @@ -1,120 +0,0 @@ -package net.jacobpeterson.alpaca.rest.endpoint.marketdata.news; - -import net.jacobpeterson.alpaca.model.endpoint.common.enums.SortDirection; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.news.NewsResponse; -import net.jacobpeterson.alpaca.rest.AlpacaClient; -import net.jacobpeterson.alpaca.rest.AlpacaClientException; -import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; -import okhttp3.HttpUrl; -import okhttp3.Request; - -import java.time.LocalDate; -import java.util.List; - -import static com.google.common.base.Preconditions.checkNotNull; - -/** - * {@link AlpacaEndpoint} for - * News - * Endpoint. - */ -public class NewsEndpoint extends AlpacaEndpoint { - - /** - * Instantiates a new {@link AlpacaEndpoint}. - * - * @param alpacaClient the {@link AlpacaClient} - */ - public NewsEndpoint(AlpacaClient alpacaClient) { - super(alpacaClient, "news"); - } - - /** - * Gets latest list of news {@link NewsResponse}. - * @return the {@link NewsResponse} - * @throws AlpacaClientException thrown for {@link AlpacaClientException} - */ - public NewsResponse getLatestNews() throws AlpacaClientException { - return this.getLatestNews(null, null, null, null, null, null, null, null); - } - - /** - * Gets latest list of news {@link NewsResponse} for specific symbols. - * @param symbols list of symbols to query news for - * @return the {@link NewsResponse} - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public NewsResponse getLatestNews(List symbols) throws AlpacaClientException { - checkNotNull(symbols); - return this.getLatestNews(null, null, null, symbols, null, null, null, null); - } - - /** - * Gets latest list of news {@link NewsResponse} for specified token. - * @param pageToken the ID of the end of your current page of results. (See the section on paging.) - * @return the {@link NewsResponse} - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public NewsResponse getLatestNewsPaged(String pageToken) throws AlpacaClientException { - checkNotNull(pageToken); - return this.getLatestNews(null, null, null, null, null, null, null, pageToken); - } - - /** - * Gets latest list of news {@link NewsResponse} - * @param start The inclusive start of the interval - * @param end the inclusive end of the interval - * @param sortDirection sort articles by updated date - * @param symbols list of symbols to query news for - * @param limit Limit of news items to be returned for given page - * @param includeContent Boolean indicator to include content for news articles (if available) - * @param excludeContentless Boolean indicator to exclude news articles that do not contain content - * @param pageToken the ID of the end of your current page of results. (See the section on paging.) - * @return the {@link NewsResponse} - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public NewsResponse getLatestNews( - LocalDate start, LocalDate end, - SortDirection sortDirection, - List symbols, - Integer limit, - Boolean includeContent, - Boolean excludeContentless, - String pageToken) throws AlpacaClientException { - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment); - - if (start != null) { - urlBuilder.addQueryParameter("start", start.toString()); - } - if (end != null) { - urlBuilder.addQueryParameter("end", end.toString()); - } - if (sortDirection != null) { - urlBuilder.addQueryParameter("sort", sortDirection.value()); - } - if (symbols != null && !symbols.isEmpty()) { - urlBuilder.addQueryParameter("symbols", String.join(",", symbols)); - } - if (limit != null) { - urlBuilder.addQueryParameter("limit", Integer.toString(limit)); - } - if (includeContent != null) { - urlBuilder.addQueryParameter("include_content", Boolean.toString(includeContent)); - } - if (excludeContentless != null) { - urlBuilder.addQueryParameter("exclude_contentless", Boolean.toString(excludeContentless)); - } - if (pageToken != null) { - urlBuilder.addQueryParameter("page_token", pageToken); - } - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - - return alpacaClient.requestObject(request, NewsResponse.class); - - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/stock/StockMarketDataEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/stock/StockMarketDataEndpoint.java deleted file mode 100644 index 3111dde7..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/stock/StockMarketDataEndpoint.java +++ /dev/null @@ -1,368 +0,0 @@ -package net.jacobpeterson.alpaca.rest.endpoint.marketdata.stock; - -import com.google.gson.reflect.TypeToken; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.historical.bar.enums.BarTimePeriod; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.MultiStockBarsResponse; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.StockBar; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.StockBarsResponse; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.enums.BarAdjustment; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.enums.BarFeed; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.LatestStockQuoteResponse; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.StockQuote; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.StockQuotesResponse; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.snapshot.Snapshot; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.LatestStockTradeResponse; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.StockTrade; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.StockTradesResponse; -import net.jacobpeterson.alpaca.rest.AlpacaClient; -import net.jacobpeterson.alpaca.rest.AlpacaClientException; -import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; -import net.jacobpeterson.alpaca.util.format.FormatUtil; -import okhttp3.HttpUrl; -import okhttp3.Request; - -import java.lang.reflect.Type; -import java.time.ZonedDateTime; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; - -/** - * {@link AlpacaEndpoint} for - * Historical - * Market Data API v2. - */ -public class StockMarketDataEndpoint extends AlpacaEndpoint { - - private static final Type SNAPSHOTS_OF_STRINGS_HASHMAP_TYPE = - new TypeToken>() {}.getType(); - - /** - * Instantiates a new {@link StockMarketDataEndpoint}. - * - * @param alpacaClient the {@link AlpacaClient} - */ - public StockMarketDataEndpoint(AlpacaClient alpacaClient) { - super(alpacaClient, "stocks"); - } - - /** - * Gets {@link StockTrade} historical data for the requested security. - * - * @param symbol the symbol to query for - * @param start filter data equal to or after this {@link ZonedDateTime}. Fractions of a second are not - * accepted. - * @param end filter data equal to or before this {@link ZonedDateTime}. Fractions of a second are not - * accepted. - * @param limit number of data points to return. Must be in range 1-10000, defaults to 1000 if null - * is given - * @param pageToken pagination token to continue from - * - * @return the {@link StockTradesResponse} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public StockTradesResponse getTrades(String symbol, ZonedDateTime start, ZonedDateTime end, Integer limit, - String pageToken) throws AlpacaClientException { - checkNotNull(symbol); - checkNotNull(start); - checkNotNull(end); - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment) - .addPathSegment(symbol) - .addPathSegment("trades"); - - urlBuilder.addQueryParameter("start", FormatUtil.toRFC3339Format(start)); - urlBuilder.addQueryParameter("end", FormatUtil.toRFC3339Format(end)); - - if (limit != null) { - urlBuilder.addQueryParameter("limit", limit.toString()); - } - - if (pageToken != null) { - urlBuilder.addQueryParameter("page_token", pageToken); - } - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, StockTradesResponse.class); - } - - /** - * Gets the latest {@link StockTrade} for the requested security. - * - * @param symbol the symbol to query for - * - * @return the {@link LatestStockTradeResponse} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public LatestStockTradeResponse getLatestTrade(String symbol) throws AlpacaClientException { - checkNotNull(symbol); - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment) - .addPathSegment(symbol) - .addPathSegment("trades") - .addPathSegment("latest"); - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, LatestStockTradeResponse.class); - } - - /** - * Gets {@link StockQuote} (NBBO or National Best Bid and Offer) historical data for the requested security. - * - * @param symbol the symbol to query for - * @param start filter data equal to or after this {@link ZonedDateTime}. Fractions of a second are not - * accepted. - * @param end filter data equal to or before this {@link ZonedDateTime}. Fractions of a second are not - * accepted. - * @param limit number of data points to return. Must be in range 1-10000, defaults to 1000 if null - * is given - * @param pageToken pagination token to continue from - * - * @return the {@link StockQuotesResponse} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public StockQuotesResponse getQuotes(String symbol, ZonedDateTime start, ZonedDateTime end, Integer limit, - String pageToken) throws AlpacaClientException { - checkNotNull(symbol); - checkNotNull(start); - checkNotNull(end); - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment) - .addPathSegment(symbol) - .addPathSegment("quotes"); - - urlBuilder.addQueryParameter("start", FormatUtil.toRFC3339Format(start)); - urlBuilder.addQueryParameter("end", FormatUtil.toRFC3339Format(end)); - - if (limit != null) { - urlBuilder.addQueryParameter("limit", limit.toString()); - } - - if (pageToken != null) { - urlBuilder.addQueryParameter("page_token", pageToken); - } - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, StockQuotesResponse.class); - } - - /** - * Gets the latest {@link StockQuote} for the requested security. - * - * @param symbol the symbol to query for - * - * @return the {@link LatestStockQuoteResponse} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public LatestStockQuoteResponse getLatestQuote(String symbol) throws AlpacaClientException { - checkNotNull(symbol); - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment) - .addPathSegment(symbol) - .addPathSegment("quotes") - .addPathSegment("latest"); - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, LatestStockQuoteResponse.class); - } - - /** - * Gets {@link StockBar} aggregate historical data for the requested security. - * - * @param symbol the symbol to query for - * @param start filter data equal to or after this {@link ZonedDateTime}. Fractions of a second are - * not accepted. - * @param end filter data equal to or before this {@link ZonedDateTime}. Fractions of a second are - * not accepted. - * @param limit number of data points to return. Must be in range 1-10000, defaults to 1000 if - * null is given - * @param pageToken pagination token to continue from - * @param barTimePeriodDuration the duration for the given barTimePeriod parameter. e.g. for - * 15Min bars, you would supply 15 for this parameter and - * {@link BarTimePeriod#MINUTE} for the barTimePeriod parameter. - * @param barTimePeriod the {@link BarTimePeriod}. e.g. for 15Min bars, you would supply - * {@link BarTimePeriod#MINUTE} for this parameter and 15 for the - * barTimePeriodDuration parameter. - * @param barAdjustment specifies the corporate action adjustment for the stocks. Default value is - * {@link BarAdjustment#RAW} - * @param barFeed defaults to {@link BarFeed#IEX} for Free users and {@link BarFeed#SIP} for users - * with an Unlimited subscription - * - * @return the {@link StockBarsResponse} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public StockBarsResponse getBars(String symbol, ZonedDateTime start, ZonedDateTime end, Integer limit, - String pageToken, int barTimePeriodDuration, BarTimePeriod barTimePeriod, BarAdjustment barAdjustment, - BarFeed barFeed) throws AlpacaClientException { - checkNotNull(symbol); - checkNotNull(start); - checkNotNull(end); - checkNotNull(barTimePeriod); - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment) - .addPathSegment(symbol) - .addPathSegment("bars"); - - urlBuilder.addQueryParameter("start", FormatUtil.toRFC3339Format(start)); - urlBuilder.addQueryParameter("end", FormatUtil.toRFC3339Format(end)); - - if (limit != null) { - urlBuilder.addQueryParameter("limit", limit.toString()); - } - - if (pageToken != null) { - urlBuilder.addQueryParameter("page_token", pageToken); - } - - urlBuilder.addQueryParameter("timeframe", barTimePeriodDuration + barTimePeriod.toString()); - - if (barAdjustment != null) { - urlBuilder.addQueryParameter("adjustment", barAdjustment.toString()); - } - - if (barFeed != null) { - urlBuilder.addQueryParameter("feed", barFeed.toString()); - } - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, StockBarsResponse.class); - } - - /** - * Gets {@link StockBar} aggregate historical data for the requested securities. - * - * @param symbols a {@link Collection} of symbols to query for - * @param start filter data equal to or after this {@link ZonedDateTime}. Fractions of a second are - * not accepted. - * @param end filter data equal to or before this {@link ZonedDateTime}. Fractions of a second are - * not accepted. - * @param limit number of data points to return. Must be in range 1-10000, defaults to 1000 if - * null is given - * @param pageToken pagination token to continue from - * @param barTimePeriodDuration the duration for the given barTimePeriod parameter. e.g. for - * 15Min bars, you would supply 15 for this parameter and - * {@link BarTimePeriod#MINUTE} for the barTimePeriod parameter. - * @param barTimePeriod the {@link BarTimePeriod}. e.g. for 15Min bars, you would supply - * {@link BarTimePeriod#MINUTE} for this parameter and 15 for the - * barTimePeriodDuration parameter. - * @param barAdjustment specifies the corporate action adjustment for the stocks. Default value is - * {@link BarAdjustment#RAW} - * @param barFeed defaults to {@link BarFeed#IEX} for Free users and {@link BarFeed#SIP} for users - * with an Unlimited subscription - * - * @return the {@link MultiStockBarsResponse} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public MultiStockBarsResponse getBars(Collection symbols, ZonedDateTime start, ZonedDateTime end, - Integer limit, String pageToken, int barTimePeriodDuration, BarTimePeriod barTimePeriod, - BarAdjustment barAdjustment, BarFeed barFeed) throws AlpacaClientException { - checkNotNull(symbols); - checkArgument(!symbols.isEmpty(), "'symbols' cannot be empty!"); - checkNotNull(start); - checkNotNull(end); - checkNotNull(barTimePeriod); - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment) - .addPathSegment("bars"); - - urlBuilder.addQueryParameter("symbols", String.join(",", symbols)); - urlBuilder.addQueryParameter("start", FormatUtil.toRFC3339Format(start)); - urlBuilder.addQueryParameter("end", FormatUtil.toRFC3339Format(end)); - - if (limit != null) { - urlBuilder.addQueryParameter("limit", limit.toString()); - } - - if (pageToken != null) { - urlBuilder.addQueryParameter("page_token", pageToken); - } - - urlBuilder.addQueryParameter("timeframe", barTimePeriodDuration + barTimePeriod.toString()); - - if (barAdjustment != null) { - urlBuilder.addQueryParameter("adjustment", barAdjustment.toString()); - } - - if (barFeed != null) { - urlBuilder.addQueryParameter("feed", barFeed.toString()); - } - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, MultiStockBarsResponse.class); - } - - /** - * Gets {@link Snapshot}s of the requested securities. - * - * @param symbols a {@link Collection} of symbols to query for - * - * @return a {@link Map} with they keys being the symbol and their values being the {@link Snapshot} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Map getSnapshots(Collection symbols) throws AlpacaClientException { - checkNotNull(symbols); - checkArgument(!symbols.isEmpty(), "'symbols' cannot be empty!"); - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment) - .addPathSegment("snapshots"); - - urlBuilder.addQueryParameter("symbols", String.join(",", symbols)); - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, SNAPSHOTS_OF_STRINGS_HASHMAP_TYPE); - } - - /** - * Gets a {@link Snapshot} of the requested security. - * - * @param symbol the symbol to query for - * - * @return the {@link Snapshot} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Snapshot getSnapshot(String symbol) throws AlpacaClientException { - checkNotNull(symbol); - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment) - .addPathSegment(symbol) - .addPathSegment("snapshot"); - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, Snapshot.class); - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/orders/OrdersEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/orders/OrdersEndpoint.java deleted file mode 100644 index b2a0563d..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/orders/OrdersEndpoint.java +++ /dev/null @@ -1,651 +0,0 @@ -package net.jacobpeterson.alpaca.rest.endpoint.orders; - -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; -import com.google.gson.reflect.TypeToken; -import net.jacobpeterson.alpaca.model.endpoint.common.enums.SortDirection; -import net.jacobpeterson.alpaca.model.endpoint.orders.CancelledOrder; -import net.jacobpeterson.alpaca.model.endpoint.orders.Order; -import net.jacobpeterson.alpaca.model.endpoint.orders.enums.CurrentOrderStatus; -import net.jacobpeterson.alpaca.model.endpoint.orders.enums.OrderClass; -import net.jacobpeterson.alpaca.model.endpoint.orders.enums.OrderSide; -import net.jacobpeterson.alpaca.model.endpoint.orders.enums.OrderStatus; -import net.jacobpeterson.alpaca.model.endpoint.orders.enums.OrderTimeInForce; -import net.jacobpeterson.alpaca.model.endpoint.orders.enums.OrderType; -import net.jacobpeterson.alpaca.rest.AlpacaClient; -import net.jacobpeterson.alpaca.rest.AlpacaClientException; -import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; -import net.jacobpeterson.alpaca.util.format.FormatUtil; -import net.jacobpeterson.alpaca.util.okhttp.JSONBodyBuilder; -import okhttp3.HttpUrl; -import okhttp3.Request; - -import java.lang.reflect.Type; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; -import static net.jacobpeterson.alpaca.rest.AlpacaClient.STATUS_CODE_200_OR_204; -import static net.jacobpeterson.alpaca.rest.AlpacaClient.STATUS_CODE_200_OR_207; - -/** - * {@link AlpacaEndpoint} for Orders. - */ -public class OrdersEndpoint extends AlpacaEndpoint { - - private static final Type ORDER_ARRAYLIST_TYPE = new TypeToken>() {}.getType(); - private static final Type CANCELLED_ORDER_ARRAYLIST_TYPE = new TypeToken>() {}.getType(); - - /** - * Instantiates a new {@link OrdersEndpoint}. - * - * @param alpacaClient the {@link AlpacaClient} - */ - public OrdersEndpoint(AlpacaClient alpacaClient) { - super(alpacaClient, "orders"); - } - - /** - * Retrieves a list of {@link Order}s for the account, filtered by the supplied query parameters. - * - * @param status {@link CurrentOrderStatus} to be queried. Defaults to {@link CurrentOrderStatus#OPEN}. - * @param limit the maximum number of orders in response. Defaults to 50 and max is 500. - * @param after the response will include only ones submitted after this timestamp (exclusive) - * @param until the response will include only ones submitted until this timestamp (exclusive) - * @param direction the chronological order of response based on the submission time. Defaults to - * {@link SortDirection#DESCENDING}. - * @param nested if true, the result will roll up multi-leg orders under the legs field of primary order. - * @param symbols a {@link Collection} of symbols to filter by (e.g. "AAPL,TSLA,MSFT"). A currency pair is - * required for crypto orders (e.g. "BTCUSD,BCHUSD,LTCUSD,ETCUSD"). null for no - * filter. - * - * @return a {@link List} of {@link Order}s - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public List get(CurrentOrderStatus status, Integer limit, ZonedDateTime after, ZonedDateTime until, - SortDirection direction, Boolean nested, Collection symbols) throws AlpacaClientException { - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment); - - if (status != null) { - urlBuilder.addQueryParameter("status", status.toString()); - } - - if (limit != null) { - urlBuilder.addQueryParameter("limit", limit.toString()); - } - - if (after != null) { - urlBuilder.addQueryParameter("after", after.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); - } - - if (until != null) { - urlBuilder.addQueryParameter("until", until.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); - } - - if (direction != null) { - urlBuilder.addQueryParameter("direction", direction.toString()); - } - - if (nested != null) { - urlBuilder.addQueryParameter("nested", nested.toString()); - } - - if (symbols != null && !symbols.isEmpty()) { - urlBuilder.addQueryParameter("symbols", String.join(",", symbols)); - } - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, ORDER_ARRAYLIST_TYPE); - } - - /** - * Places a new order for the given account. An order request may be rejected if the account is not authorized for - * trading, or if the tradable balance is insufficient to fill the order. Note: many of the parameters for this - * method can be set to null if they aren't required for the requested {@link OrderType}. - * - * @param symbol symbol or asset ID to identify the asset to trade - * @param quantity number of shares to trade. Can be fractional. - * @param notional dollar amount to trade. Cannot work with quantity. Can only work for - * {@link OrderType#MARKET} and {@link OrderTimeInForce#DAY}. - * @param side the {@link OrderSide} - * @param type the {@link OrderType} - * @param timeInForce the {@link OrderTimeInForce} - * @param limitPrice required if type is {@link OrderType#LIMIT} or {@link OrderType#STOP_LIMIT} - * @param stopPrice required if type is {@link OrderType#STOP} or {@link OrderType#STOP_LIMIT} - * @param trailPrice this or trail_percent is required if type is - * {@link OrderType#TRAILING_STOP} - * @param trailPercent this or trail_price is required if type is - * {@link OrderType#TRAILING_STOP} - * @param extendedHours (default) false. If true, order will be eligible to execute in premarket/afterhours. - * Only works with type {@link OrderType#LIMIT} and {@link OrderTimeInForce#DAY}. - * @param clientOrderId a unique identifier for the order. Automatically generated if null. - * @param orderClass the {@link OrderClass}. For details of non-simple order classes, please see "Bracket - * Order Overview" on the Alpaca Docs. - * @param takeProfitLimitPrice additional parameter for take-profit leg of advanced orders. Required for - * {@link OrderClass#BRACKET}. - * @param stopLossStopPrice additional parameters for stop-loss leg of advanced orders. Required for - * {@link OrderClass#BRACKET}. - * @param stopLossLimitPrice additional parameters for stop-loss leg of advanced orders. The stop-loss order - * becomes a stop-limit order if specified. - * - * @return the requested {@link Order} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Order requestOrder(String symbol, Double quantity, Double notional, OrderSide side, OrderType type, - OrderTimeInForce timeInForce, Double limitPrice, Double stopPrice, Double trailPrice, Double trailPercent, - Boolean extendedHours, String clientOrderId, OrderClass orderClass, Double takeProfitLimitPrice, - Double stopLossStopPrice, Double stopLossLimitPrice) - throws AlpacaClientException { - checkNotNull(symbol); - checkState(quantity != null ^ notional != null, "Either 'quantity' or 'notional' are required."); - checkNotNull(side); - checkNotNull(type); - checkNotNull(timeInForce); - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment); - - JSONBodyBuilder jsonBodyBuilder = new JSONBodyBuilder(); - jsonBodyBuilder.appendJSONBodyProperty("symbol", symbol); - jsonBodyBuilder.appendJSONBodyProperty("side", side.toString()); - jsonBodyBuilder.appendJSONBodyProperty("type", type.toString()); - jsonBodyBuilder.appendJSONBodyProperty("time_in_force", timeInForce.toString()); - - if (quantity != null) { - jsonBodyBuilder.appendJSONBodyProperty("qty", quantity.toString()); - } else { - jsonBodyBuilder.appendJSONBodyProperty("notional", notional.toString()); - } - - if (limitPrice != null) { - jsonBodyBuilder.appendJSONBodyProperty("limit_price", FormatUtil.toCurrencyFormat(limitPrice)); - } - - if (stopPrice != null) { - jsonBodyBuilder.appendJSONBodyProperty("stop_price", FormatUtil.toCurrencyFormat(stopPrice)); - } - - if (trailPrice != null) { - jsonBodyBuilder.appendJSONBodyProperty("trail_price", FormatUtil.toCurrencyFormat(trailPrice)); - } - - if (trailPercent != null) { - jsonBodyBuilder.appendJSONBodyProperty("trail_percent", FormatUtil.toCurrencyFormat(trailPercent)); - } - - if (extendedHours != null) { - jsonBodyBuilder.appendJSONBodyJSONProperty("extended_hours", new JsonPrimitive(extendedHours)); - } - - if (clientOrderId != null) { - jsonBodyBuilder.appendJSONBodyProperty("client_order_id", clientOrderId); - } - - if (orderClass != null) { - jsonBodyBuilder.appendJSONBodyProperty("order_class", orderClass.toString()); - } - - if (takeProfitLimitPrice != null) { - JsonObject takeProfitObject = new JsonObject(); - takeProfitObject.addProperty("limit_price", takeProfitLimitPrice); - jsonBodyBuilder.appendJSONBodyJSONProperty("take_profit", takeProfitObject); - } - - if (stopLossStopPrice != null || stopLossLimitPrice != null) { - JsonObject stopLossObject = new JsonObject(); - - if (stopLossStopPrice != null) { - stopLossObject.addProperty("stop_price", stopLossStopPrice); - } - - if (stopLossLimitPrice != null) { - stopLossObject.addProperty("limit_price", stopLossLimitPrice); - } - - jsonBodyBuilder.appendJSONBodyJSONProperty("stop_loss", stopLossObject); - } - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .post(jsonBodyBuilder.build()) - .build(); - return alpacaClient.requestObject(request, Order.class); - } - - /** - * A market order is a request to buy or sell a security at the currently available market price. It provides the - * most likely method of filling an order. Market orders fill nearly instantaneously. This method calls - * {@link #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, - * Double, Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#MARKET} so check the Javadoc - * for that method for parameter details. - * - * @return the requested {@link Order} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Order requestMarketOrder(String symbol, Integer quantity, OrderSide side, OrderTimeInForce timeInForce) - throws AlpacaClientException { - return requestOrder(symbol, quantity.doubleValue(), null, side, OrderType.MARKET, timeInForce, null, null, - null, null, null, null, OrderClass.SIMPLE, null, null, null); - } - - /** - * A market order is a request to buy or sell a security at the currently available market price. It provides the - * most likely method of filling an order. Market orders fill nearly instantaneously. This method calls - * {@link #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, - * Double, Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#MARKET} and - * {@link OrderTimeInForce#GOOD_UNTIL_CANCELLED} with a fractional quantity so check the Javadoc for that method for - * parameter details. - * - * @return the requested {@link Order} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Order requestFractionalMarketOrder(String symbol, Double quantity, OrderSide side) - throws AlpacaClientException { - return requestOrder(symbol, quantity, null, side, OrderType.MARKET, OrderTimeInForce.DAY, null, - null, null, - null, null, null, null, null, null, null); - } - - /** - * A market order is a request to buy or sell a security at the currently available market price. It provides the - * most likely method of filling an order. Market orders fill nearly instantaneously. This method calls - * {@link #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, - * Double, Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#MARKET} and - * {@link OrderTimeInForce#GOOD_UNTIL_CANCELLED} with a notional dollar amount so check the Javadoc for that method - * for the parameter details. - * - * @return the requested {@link Order} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Order requestNotionalMarketOrder(String symbol, Double notional, OrderSide side) - throws AlpacaClientException { - return requestOrder(symbol, null, notional, side, OrderType.MARKET, OrderTimeInForce.GOOD_UNTIL_CANCELLED, null, - null, null, - null, null, null, null, null, null, null); - } - - /** - * A limit order is an order to buy or sell at a specified price or better. A buy limit order (a limit order to buy) - * is executed at the specified limit price or lower (i.e., better). Conversely, a sell limit order (a limit order - * to sell) is executed at the specified limit price or higher (better). Unlike a market order, you have to specify - * the limit price parameter when submitting your order. This method calls - * {@link #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, - * Double, Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#LIMIT} so check the Javadoc - * for that method for the parameter details. - * - * @return the requested {@link Order} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Order requestLimitOrder(String symbol, Double quantity, OrderSide side, OrderTimeInForce timeInForce, - Double limitPrice, Boolean extendedHours) throws AlpacaClientException { - return requestOrder(symbol, quantity, null, side, OrderType.LIMIT, timeInForce, limitPrice, - null, null, null, extendedHours, null, OrderClass.SIMPLE, null, null, null); - } - - /** - * A stop (market) order is an order to buy or sell a security when its price moves past a particular point, - * ensuring a higher probability of achieving a predetermined entry or exit price. Once the market price crosses the - * specified stop price, the stop order becomes a market order. Alpaca converts buy stop orders into stop limit - * orders with a limit price that is 4% higher than a stop price < $50 (or 2.5% higher than a stop price >= - * $50). Sell stop orders are not converted into stop limit orders. This method calls - * {@link #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, - * Double, Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#STOP} so check the Javadoc - * for that method for parameter details. - * - * @return the requested {@link Order} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Order requestStopOrder(String symbol, Integer quantity, OrderSide side, OrderTimeInForce timeInForce, - Double stopPrice, Boolean extendedHours) throws AlpacaClientException { - return requestOrder(symbol, quantity.doubleValue(), null, side, OrderType.STOP, timeInForce, null, stopPrice, - null, null, extendedHours, null, OrderClass.SIMPLE, null, null, null); - } - - /** - * A stop-limit order is a conditional trade over a set time frame that combines the features of a stop order with - * those of a limit order and is used to mitigate risk. The stop-limit order will be executed at a specified limit - * price, or better, after a given stop price has been reached. Once the stop price is reached, the stop-limit order - * becomes a limit order to buy or sell at the limit price or better. This method calls - * {@link #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, - * Double, Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#STOP_LIMIT} so check the - * Javadoc for that method for parameter details. - * - * @return the requested {@link Order} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Order requestStopLimitOrder(String symbol, Integer quantity, OrderSide side, OrderTimeInForce timeInForce, - Double limitPrice, Double stopPrice, Boolean extendedHours) throws AlpacaClientException { - return requestOrder(symbol, quantity.doubleValue(), null, side, OrderType.STOP_LIMIT, timeInForce, - limitPrice, stopPrice, null, null, extendedHours, null, OrderClass.SIMPLE, null, null, null); - } - - /** - * A bracket order is a chain of three orders that can be used to manage your position entry and exit. It is a - * common use case of an OTOCO (One Triggers OCO {One Cancels Other}) order. This method calls - * {@link #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, - * Double, Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#MARKET} and with parameters - * for a bracket order so check the Javadoc for that method for parameter details. - * - * @return the requested {@link Order} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Order requestMarketBracketOrder(String symbol, Integer quantity, OrderSide side, - OrderTimeInForce timeInForce, Double takeProfitLimitPrice, Double stopLossStopPrice, - Double stopLossLimitPrice) throws AlpacaClientException { - return requestOrder(symbol, quantity.doubleValue(), null, side, OrderType.MARKET, timeInForce, null, null, - null, null, null, null, OrderClass.BRACKET, takeProfitLimitPrice, stopLossStopPrice, - stopLossLimitPrice); - } - - /** - * A bracket order is a chain of three orders that can be used to manage your position entry and exit. It is a - * common use case of an OTOCO (One Triggers OCO {One Cancels Other}) order. This method calls - * {@link #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, - * Double, Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#LIMIT} and with parameters - * for a bracket order so check the Javadoc for that method for parameter details. - * - * @return the requested {@link Order} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Order requestLimitBracketOrder(String symbol, Integer quantity, OrderSide side, - OrderTimeInForce timeInForce, Double limitPrice, Boolean extendedHours, Double takeProfitLimitPrice, - Double stopLossStopPrice, Double stopLossLimitPrice) throws AlpacaClientException { - return requestOrder(symbol, quantity.doubleValue(), null, side, OrderType.LIMIT, timeInForce, limitPrice, - null, null, null, extendedHours, null, OrderClass.BRACKET, takeProfitLimitPrice, stopLossStopPrice, - stopLossLimitPrice); - } - - /** - * OCO (One-Cancels-Other) is another type of advanced order type. This is a set of two orders with the same side - * (buy/buy or sell/sell) and currently only exit order is supported. In other words, this is the second part of the - * bracket orders where the entry order is already filled, and you can submit the take-profit and stop-loss in one - * order submission. This method calls - * {@link #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, - * Double, Boolean, String, OrderClass, Double, Double, Double)} with parameters for a - * {@link OrderClass#ONE_CANCELS_OTHER} so check the Javadoc for that method for the parameter details. - * - * @return the requested {@link Order} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Order requestOCOOrder(String symbol, Integer quantity, OrderSide side, - OrderTimeInForce timeInForce, Boolean extendedHours, Double takeProfitLimitPrice, - Double stopLossStopPrice, Double stopLossLimitPrice) throws AlpacaClientException { - return requestOrder(symbol, quantity.doubleValue(), null, side, OrderType.LIMIT, timeInForce, null, null, - null, null, extendedHours, null, OrderClass.ONE_CANCELS_OTHER, takeProfitLimitPrice, stopLossStopPrice, - stopLossLimitPrice); - } - - /** - * OTO (One-Triggers-Other) is a variant of bracket order. It takes one of the take-profit or stop-loss order in - * addition to the entry order. This method calls - * {@link #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, - * Double, Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#LIMIT} and with parameters - * for a {@link OrderClass#ONE_CANCELS_OTHER} so check the Javadoc for that method for parameter details. - * - * @return the requested {@link Order} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Order requestOTOMarketOrder(String symbol, Integer quantity, OrderSide side, - OrderTimeInForce timeInForce, Double takeProfitLimitPrice, Double stopLossStopPrice, - Double stopLossLimitPrice) throws AlpacaClientException { - return requestOrder(symbol, quantity.doubleValue(), null, side, OrderType.MARKET, timeInForce, null, null, - null, null, null, null, OrderClass.ONE_CANCELS_OTHER, takeProfitLimitPrice, stopLossStopPrice, - stopLossLimitPrice); - } - - /** - * OTO (One-Triggers-Other) is a variant of bracket order. It takes one of the take-profit or stop-loss order in - * addition to the entry order. This method calls - * {@link #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, - * Double, Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#LIMIT} and with parameters - * for a {@link OrderClass#ONE_CANCELS_OTHER} so check the Javadoc for that method for parameter details. - * - * @return the requested {@link Order} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Order requestOTOLimitOrder(String symbol, Integer quantity, OrderSide side, - OrderTimeInForce timeInForce, Double limitPrice, Boolean extendedHours, Double takeProfitLimitPrice, - Double stopLossStopPrice, Double stopLossLimitPrice) throws AlpacaClientException { - return requestOrder(symbol, quantity.doubleValue(), null, side, OrderType.LIMIT, timeInForce, limitPrice, - null, null, null, extendedHours, null, OrderClass.ONE_CANCELS_OTHER, takeProfitLimitPrice, - stopLossStopPrice, stopLossLimitPrice); - } - - /** - * OTO (One-Triggers-Other) is a variant of bracket order. It takes one of the take-profit or stop-loss order in - * addition to the entry order. This method calls - * {@link #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, - * Double, Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#STOP} and with parameters for - * a {@link OrderClass#ONE_CANCELS_OTHER} so check the Javadoc for that method for parameter details. - * - * @return the requested {@link Order} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Order requestOTOStopOrder(String symbol, Integer quantity, OrderSide side, - OrderTimeInForce timeInForce, Double stopPrice, Boolean extendedHours, Double takeProfitLimitPrice, - Double stopLossStopPrice, Double stopLossLimitPrice) throws AlpacaClientException { - return requestOrder(symbol, quantity.doubleValue(), null, side, OrderType.STOP, timeInForce, null, stopPrice, - null, null, extendedHours, null, OrderClass.ONE_CANCELS_OTHER, takeProfitLimitPrice, stopLossStopPrice, - stopLossLimitPrice); - } - - /** - * OTO (One-Triggers-Other) is a variant of bracket order. It takes one of the take-profit or stop-loss order in - * addition to the entry order. This method calls - * {@link #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, - * Double, Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#STOP_LIMIT} and with - * parameters for a {@link OrderClass#ONE_CANCELS_OTHER} so check the Javadoc for that method for parameter - * details. - * - * @return the requested {@link Order} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Order requestOTOStopLimitOrder(String symbol, Integer quantity, OrderSide side, - OrderTimeInForce timeInForce, Double limitPrice, Double stopPrice, Boolean extendedHours, - Double takeProfitLimitPrice, Double stopLossStopPrice, Double stopLossLimitPrice) - throws AlpacaClientException { - return requestOrder(symbol, quantity.doubleValue(), null, side, OrderType.STOP_LIMIT, timeInForce, - limitPrice, stopPrice, null, null, extendedHours, null, OrderClass.ONE_CANCELS_OTHER, - takeProfitLimitPrice, stopLossStopPrice, stopLossLimitPrice); - } - - /** - * Trailing stop orders allow you to continuously and automatically keep updating the stop price threshold based on - * the stock price movement. This method calls - * {@link #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, - * Double, Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#TRAILING_STOP} and with - * parameters for a {@link OrderType#TRAILING_STOP} so check the Javadoc for that method for parameter details. - * - * @return the requested {@link Order} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Order requestTrailingStopPriceOrder(String symbol, Integer quantity, OrderSide side, - OrderTimeInForce timeInForce, Double trailPrice, Boolean extendedHours) throws AlpacaClientException { - return requestOrder(symbol, quantity.doubleValue(), null, side, OrderType.TRAILING_STOP, timeInForce, null, - null, trailPrice, null, extendedHours, null, null, null, null, null); - } - - /** - * Trailing stop orders allow you to continuously and automatically keep updating the stop price threshold based on - * the stock price movement. This method calls - * {@link #requestOrder(String, Double, Double, OrderSide, OrderType, OrderTimeInForce, Double, Double, Double, - * Double, Boolean, String, OrderClass, Double, Double, Double)} with {@link OrderType#TRAILING_STOP} and with - * parameters for a {@link OrderType#TRAILING_STOP} so check the Javadoc for that method for parameter details. - * - * @return the requested {@link Order} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Order requestTrailingStopPercentOrder(String symbol, Integer quantity, OrderSide side, - OrderTimeInForce timeInForce, Double trailPercent, Boolean extendedHours) throws AlpacaClientException { - return requestOrder(symbol, quantity.doubleValue(), null, side, OrderType.TRAILING_STOP, timeInForce, null, - null, null, trailPercent, extendedHours, null, null, null, null, null); - } - - /** - * Retrieves a single {@link Order} for the given orderID. - * - * @param orderID the {@link Order#getId()} - * @param nested if true, the result will roll up multi-leg orders under the legs field of primary order. - * - * @return the {@link Order} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Order get(String orderID, Boolean nested) throws AlpacaClientException { - checkNotNull(orderID); - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment) - .addPathSegment(orderID); - - if (nested != null) { - urlBuilder.addQueryParameter("nested", nested.toString()); - } - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, Order.class); - } - - /** - * Retrieves a single {@link Order} for the given clientOrderId. - * - * @param clientOrderID the {@link Order#getClientOrderId()} - * - * @return the {@link Order} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Order getByClientID(String clientOrderID) throws AlpacaClientException { - checkNotNull(clientOrderID); - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment("orders:by_client_order_id"); - urlBuilder.addQueryParameter("client_order_id", clientOrderID); - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, Order.class); - } - - /** - * Replaces a single {@link Order} with updated parameters. Each parameter overrides the corresponding attribute of - * the existing {@link Order}. The other attributes remain the same as the existing {@link Order}. - * - * @param orderID the {@link Order#getId()} - * @param quantity number of shares to trade. Can be fractional. - * @param timeInForce the {@link OrderTimeInForce} - * @param limitPrice required if type is {@link OrderType#LIMIT} or {@link OrderType#STOP_LIMIT} - * @param stopPrice required if type is {@link OrderType#STOP} or {@link OrderType#STOP_LIMIT} - * @param trail the new value of the trail_price or trail_percent value (works - * only for {@link Order#getType()} == {@link OrderType#TRAILING_STOP}) - * @param clientOrderId a unique identifier for the order. Automatically generated if null. - * - * @return a new {@link Order} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Order replace(String orderID, Integer quantity, OrderTimeInForce timeInForce, Double limitPrice, - Double stopPrice, Double trail, String clientOrderId) throws AlpacaClientException { - checkNotNull(orderID); - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment) - .addPathSegment(orderID); - - JSONBodyBuilder jsonBodyBuilder = new JSONBodyBuilder(); - - if (quantity != null) { - jsonBodyBuilder.appendJSONBodyProperty("qty", quantity.toString()); - } - - if (timeInForce != null) { - jsonBodyBuilder.appendJSONBodyProperty("time_in_force", timeInForce.toString()); - } - - if (limitPrice != null) { - jsonBodyBuilder.appendJSONBodyProperty("limit_price", FormatUtil.toCurrencyFormat(limitPrice)); - } - - if (stopPrice != null) { - jsonBodyBuilder.appendJSONBodyProperty("stop_price", FormatUtil.toCurrencyFormat(stopPrice)); - } - - if (trail != null) { - jsonBodyBuilder.appendJSONBodyProperty("trail", FormatUtil.toCurrencyFormat(trail)); - } - - if (clientOrderId != null) { - jsonBodyBuilder.appendJSONBodyProperty("client_order_id", clientOrderId); - } - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .patch(jsonBodyBuilder.build()) - .build(); - return alpacaClient.requestObject(request, Order.class); - } - - /** - * Attempts to cancel all open {@link Order}s. A response will be provided for each {@link Order} that is attempted - * to be cancelled. If an {@link Order} is no longer cancelable, the server will respond with status - * 500 and reject the request. - * - * @return a {@link List} of {@link CancelledOrder}s - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public List cancelAll() throws AlpacaClientException { - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment); - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .delete() - .build(); - return alpacaClient.requestObject(request, STATUS_CODE_200_OR_207, CANCELLED_ORDER_ARRAYLIST_TYPE); - } - - /** - * Attempts to cancel an open {@link Order}. If the {@link Order} is no longer cancelable (example: - * {@link Order#getStatus()} == {@link OrderStatus#FILLED}), the server will respond with status - * 422, and reject the request. - * - * @param orderID the {@link Order#getId()} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public void cancel(String orderID) throws AlpacaClientException { - checkNotNull(orderID); - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment) - .addPathSegment(orderID); - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .delete() - .build(); - alpacaClient.requestVoid(request, STATUS_CODE_200_OR_204); - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/portfoliohistory/PortfolioHistoryEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/portfoliohistory/PortfolioHistoryEndpoint.java deleted file mode 100644 index 722299f0..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/portfoliohistory/PortfolioHistoryEndpoint.java +++ /dev/null @@ -1,103 +0,0 @@ -package net.jacobpeterson.alpaca.rest.endpoint.portfoliohistory; - -import net.jacobpeterson.alpaca.model.endpoint.portfoliohistory.PortfolioHistory; -import net.jacobpeterson.alpaca.model.endpoint.portfoliohistory.PortfolioHistoryDataPoint; -import net.jacobpeterson.alpaca.model.endpoint.portfoliohistory.PortfolioHistoryResponse; -import net.jacobpeterson.alpaca.model.endpoint.portfoliohistory.enums.PortfolioPeriodUnit; -import net.jacobpeterson.alpaca.model.endpoint.portfoliohistory.enums.PortfolioTimeFrame; -import net.jacobpeterson.alpaca.rest.AlpacaClient; -import net.jacobpeterson.alpaca.rest.AlpacaClientException; -import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; -import net.jacobpeterson.alpaca.util.format.FormatUtil; -import okhttp3.HttpUrl; -import okhttp3.Request; - -import java.time.Instant; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; - -import static com.google.common.base.Preconditions.checkState; - -/** - * {@link AlpacaEndpoint} for - * Portfolio - * History. - */ -public class PortfolioHistoryEndpoint extends AlpacaEndpoint { - - /** - * Instantiates a new {@link PortfolioHistoryEndpoint}. - * - * @param alpacaClient the {@link AlpacaClient} - */ - public PortfolioHistoryEndpoint(AlpacaClient alpacaClient) { - super(alpacaClient, "account/portfolio/history"); - } - - /** - * Returns timeseries data about equity and profit/loss (P/L) of the account in requested timespan. - * - * @param periodLength the duration of the periodUnit. Defaults to 1. - * @param periodUnit the {@link PortfolioPeriodUnit}. Defaults to {@link PortfolioPeriodUnit#MONTH}. - * @param timeFrame the resolution of time window. If omitted, {@link PortfolioTimeFrame#ONE_MIN} for less than - * 7 days period, {@link PortfolioTimeFrame#FIFTEEN_MINUTE} for less than 30 days, or otherwise - * {@link PortfolioTimeFrame#ONE_DAY}. - * @param dateEnd the date the data is returned up to. Defaults to the current market date (rolls over at the - * market open if extended_hours is false, otherwise at 7 AM ET) - * @param extendedHours if true, include extended hours in the result. This is effective only for timeframe less - * than {@link PortfolioTimeFrame#ONE_DAY}. - * - * @return the {@link PortfolioHistory} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public PortfolioHistory get(Integer periodLength, PortfolioPeriodUnit periodUnit, PortfolioTimeFrame timeFrame, - LocalDate dateEnd, Boolean extendedHours) throws AlpacaClientException { - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegments(endpointPathSegment); - - if (periodLength != null && periodUnit != null) { - urlBuilder.addQueryParameter("period", periodLength + periodUnit.toString()); - } - - if (timeFrame != null) { - urlBuilder.addQueryParameter("timeframe", timeFrame.toString()); - } - - if (dateEnd != null) { - urlBuilder.addQueryParameter("date_end", dateEnd.format(DateTimeFormatter.ISO_DATE)); - } - - if (extendedHours != null) { - urlBuilder.addQueryParameter("extended_hours", extendedHours.toString()); - } - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - PortfolioHistoryResponse response = alpacaClient.requestObject(request, - PortfolioHistoryResponse.class); - - // Check if any response arrays differ in size - checkState(response.getTimestamp().size() == response.getEquity().size() && - response.getEquity().size() == response.getProfitLoss().size() && - response.getProfitLoss().size() == response.getProfitLossPercent().size(), - "Response arrays should not differ in size!"); - - // Add all data points into one POJO - ArrayList dataPoints = new ArrayList<>(); - for (int index = 0; index < response.getTimestamp().size(); index++) { - dataPoints.add(new PortfolioHistoryDataPoint( - Instant.ofEpochSecond(response.getTimestamp().get(index)).atZone(FormatUtil.NEW_YORK_ZONED_ID), - response.getEquity().get(index), - response.getProfitLoss().get(index), - response.getProfitLossPercent().get(index))); - } - - return new PortfolioHistory( - dataPoints, - response.getBaseValue(), - response.getTimeframe()); - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/positions/PositionsEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/positions/PositionsEndpoint.java deleted file mode 100644 index fc8eda81..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/positions/PositionsEndpoint.java +++ /dev/null @@ -1,136 +0,0 @@ -package net.jacobpeterson.alpaca.rest.endpoint.positions; - -import com.google.gson.reflect.TypeToken; -import net.jacobpeterson.alpaca.model.endpoint.assets.Asset; -import net.jacobpeterson.alpaca.model.endpoint.orders.Order; -import net.jacobpeterson.alpaca.model.endpoint.positions.ClosePositionOrder; -import net.jacobpeterson.alpaca.model.endpoint.positions.Position; -import net.jacobpeterson.alpaca.rest.AlpacaClient; -import net.jacobpeterson.alpaca.rest.AlpacaClientException; -import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; -import okhttp3.HttpUrl; -import okhttp3.Request; - -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.List; - -import static com.google.common.base.Preconditions.checkNotNull; -import static net.jacobpeterson.alpaca.rest.AlpacaClient.STATUS_CODE_200_OR_207; - -/** - * {@link AlpacaEndpoint} for Positions. - */ -public class PositionsEndpoint extends AlpacaEndpoint { - - private static final Type POSITION_ARRAYLIST_TYPE = new TypeToken>() {}.getType(); - private static final Type CLOSED_POSITION_ORDER_ARRAYLIST_TYPE = - new TypeToken>() {}.getType(); - - /** - * Instantiates a new {@link PositionsEndpoint}. - * - * @param alpacaClient the {@link AlpacaClient} - */ - public PositionsEndpoint(AlpacaClient alpacaClient) { - super(alpacaClient, "positions"); - } - - /** - * Retrieves a list of the account's open {@link Position}s. - * - * @return a {@link List} of open {@link Position}s - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public List get() throws AlpacaClientException { - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment); - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, POSITION_ARRAYLIST_TYPE); - } - - /** - * Retrieves the account's open {@link Position}s for the given symbol or {@link Asset#getId()}. - * - * @param symbolOrAssetID the symbol or {@link Asset#getId()} (required) - * - * @return the open {@link Position} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Position getBySymbol(String symbolOrAssetID) throws AlpacaClientException { - checkNotNull(symbolOrAssetID); - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment) - .addPathSegment(symbolOrAssetID); - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, Position.class); - } - - /** - * Closes (liquidates) all of the account’s open long and short {@link Position}s. A response will be provided for - * each order that is attempted to be cancelled. If an order is no longer cancelable, the server will respond with - * status 500 and reject the request. - * - * @param cancelOrders if true is specified, cancel all open orders before liquidating all positions - * - * @return a {@link List} of open {@link ClosePositionOrder}s - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public List closeAll(Boolean cancelOrders) throws AlpacaClientException { - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment); - - if (cancelOrders != null) { - urlBuilder.addQueryParameter("cancel_orders", cancelOrders.toString()); - } - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .delete() - .build(); - return alpacaClient.requestObject(request, STATUS_CODE_200_OR_207, CLOSED_POSITION_ORDER_ARRAYLIST_TYPE); - } - - /** - * Closes (liquidates) the account’s open position for the given symbol. Works for both long and short - * positions. - * - * @param symbolOrAssetID the symbol or {@link Asset#getId()} - * @param quantity the number of shares to liquidate. Can accept up to 9 decimal points. Cannot - * work with percentage. - * @param percentage the percentage of the position to liquidate. Must be between 0 and - * 100. Would only sell fractional if position is originally fractional. Can - * accept up to 9 decimal points. Cannot work with quantity. - * - * @return a closing {@link Position} {@link Order} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Order close(String symbolOrAssetID, Double quantity, Double percentage) throws AlpacaClientException { - checkNotNull(symbolOrAssetID); - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment) - .addPathSegment(symbolOrAssetID); - - if (quantity != null) { - urlBuilder.addQueryParameter("qty", quantity.toString()); - } - - if (percentage != null) { - urlBuilder.addQueryParameter("percentage", percentage.toString()); - } - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .delete() - .build(); - return alpacaClient.requestObject(request, Order.class); - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/watchlist/WatchlistEndpoint.java b/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/watchlist/WatchlistEndpoint.java deleted file mode 100644 index 605d651b..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/rest/endpoint/watchlist/WatchlistEndpoint.java +++ /dev/null @@ -1,216 +0,0 @@ -package net.jacobpeterson.alpaca.rest.endpoint.watchlist; - -import com.google.gson.JsonArray; -import com.google.gson.reflect.TypeToken; -import net.jacobpeterson.alpaca.model.endpoint.assets.Asset; -import net.jacobpeterson.alpaca.model.endpoint.watchlist.Watchlist; -import net.jacobpeterson.alpaca.rest.AlpacaClient; -import net.jacobpeterson.alpaca.rest.AlpacaClientException; -import net.jacobpeterson.alpaca.rest.endpoint.AlpacaEndpoint; -import net.jacobpeterson.alpaca.util.okhttp.JSONBodyBuilder; -import okhttp3.HttpUrl; -import okhttp3.Request; - -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; -import static net.jacobpeterson.alpaca.rest.AlpacaClient.STATUS_CODE_200_OR_204; - -/** - * {@link AlpacaEndpoint} for Watchlists - * . - */ -public class WatchlistEndpoint extends AlpacaEndpoint { - - private static final Type WATCHLIST_ARRAYLIST_TYPE = new TypeToken>() {}.getType(); - - /** - * Instantiates a new {@link WatchlistEndpoint}. - * - * @param alpacaClient the {@link AlpacaClient} - */ - public WatchlistEndpoint(AlpacaClient alpacaClient) { - super(alpacaClient, "watchlists"); - } - - /** - * Returns the list of {@link Watchlist}s registered under the account. - * - * @return a {@link List} of {@link Watchlist}s - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public List get() throws AlpacaClientException { - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment); - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, WATCHLIST_ARRAYLIST_TYPE); - } - - /** - * Creates a new {@link Watchlist} with initial set of {@link Asset}s. - * - * @param name arbitrary name string, up to 64 characters - * @param symbols set of symbols {@link String}s - * - * @return the created {@link Watchlist} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Watchlist create(String name, String... symbols) throws AlpacaClientException { - checkNotNull(name); - checkArgument(name.length() <= 64, "'name' cannot be longer than 64 characters!"); - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment); - - JSONBodyBuilder jsonBodyBuilder = new JSONBodyBuilder(); - jsonBodyBuilder.appendJSONBodyProperty("name", name); - - if (symbols != null && symbols.length != 0) { - JsonArray symbolsArray = new JsonArray(); - Arrays.stream(symbols).forEach(symbolsArray::add); - jsonBodyBuilder.appendJSONBodyJSONProperty("symbols", symbolsArray); - } - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .post(jsonBodyBuilder.build()) - .build(); - return alpacaClient.requestObject(request, Watchlist.class); - } - - /** - * Returns a {@link Watchlist} identified by the ID. - * - * @param watchlistID the {@link Watchlist#getId()} - * - * @return the {@link Watchlist} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Watchlist get(String watchlistID) throws AlpacaClientException { - checkNotNull(watchlistID); - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment) - .addPathSegment(watchlistID); - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .get() - .build(); - return alpacaClient.requestObject(request, Watchlist.class); - } - - /** - * Updates the name and/or content of a {@link Watchlist}. - * - * @param watchlistID the {@link Watchlist#getId()} - * @param name the new {@link Watchlist} name - * @param symbols the new list of symbol names to replace the {@link Watchlist} content - * - * @return the updated {@link Watchlist} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Watchlist update(String watchlistID, String name, String... symbols) - throws AlpacaClientException { - checkNotNull(watchlistID); - checkArgument(name == null || name.length() <= 64, "'name' cannot be longer than 64 characters!"); - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment) - .addPathSegment(watchlistID); - - JSONBodyBuilder jsonBodyBuilder = new JSONBodyBuilder(); - - if (name != null) { - jsonBodyBuilder.appendJSONBodyProperty("name", name); - } - - if (symbols != null && symbols.length != 0) { - JsonArray symbolsArray = new JsonArray(); - Arrays.stream(symbols).forEach(symbolsArray::add); - jsonBodyBuilder.appendJSONBodyJSONProperty("symbols", symbolsArray); - } - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .put(jsonBodyBuilder.build()) - .build(); - return alpacaClient.requestObject(request, Watchlist.class); - } - - /** - * Append an asset for the symbol to the end of {@link Watchlist} asset list. - * - * @param watchlistID the {@link Watchlist#getId()} - * @param symbol the symbol name to add to the {@link Watchlist} - * - * @return the updated {@link Watchlist} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Watchlist addAsset(String watchlistID, String symbol) throws AlpacaClientException { - checkNotNull(watchlistID); - checkNotNull(symbol); - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment) - .addPathSegment(watchlistID); - - JSONBodyBuilder jsonBodyBuilder = new JSONBodyBuilder(); - jsonBodyBuilder.appendJSONBodyProperty("symbol", symbol); - - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .post(jsonBodyBuilder.build()) - .build(); - return alpacaClient.requestObject(request, Watchlist.class); - } - - /** - * Deletes a {@link Watchlist}. This is a permanent deletion. - * - * @param watchlistID the {@link Watchlist#getId()} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public void delete(String watchlistID) throws AlpacaClientException { - checkNotNull(watchlistID); - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment) - .addPathSegment(watchlistID); - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .delete() - .build(); - alpacaClient.requestVoid(request, STATUS_CODE_200_OR_204); - } - - /** - * Delete one entry for an asset by symbol name. - * - * @param watchlistID the {@link Watchlist#getId()} - * @param symbol symbol name to remove from the {@link Watchlist} content - * - * @return the updated {@link Watchlist} - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - public Watchlist removeSymbol(String watchlistID, String symbol) throws AlpacaClientException { - checkNotNull(watchlistID); - checkNotNull(symbol); - - HttpUrl.Builder urlBuilder = alpacaClient.urlBuilder() - .addPathSegment(endpointPathSegment) - .addPathSegment(watchlistID) - .addPathSegment(symbol); - Request request = alpacaClient.requestBuilder(urlBuilder.build()) - .delete() - .build(); - return alpacaClient.requestObject(request, STATUS_CODE_200_OR_204, Watchlist.class); - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/util/doc/Documentation2JSONSchema.java b/src/main/java/net/jacobpeterson/alpaca/util/doc/Documentation2JSONSchema.java deleted file mode 100644 index ba0e2208..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/util/doc/Documentation2JSONSchema.java +++ /dev/null @@ -1,130 +0,0 @@ -package net.jacobpeterson.alpaca.util.doc; - -import org.w3c.dom.Document; -import org.w3c.dom.NodeList; -import org.xml.sax.SAXException; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.xpath.XPath; -import javax.xml.xpath.XPathConstants; -import javax.xml.xpath.XPathExpression; -import javax.xml.xpath.XPathExpressionException; -import javax.xml.xpath.XPathFactory; -import java.io.ByteArrayInputStream; -import java.io.IOException; - -/** - * The {@link Documentation2JSONSchema} is used strictly to expedite the process of creating JSON schemas for POJO - * generation in this library using Alpaca's website documentation. - */ -public class Documentation2JSONSchema { - - /** - * This is a standalone helper method that converts properties HTML copied from the Alpaca docs page into a json - * schema property list that includes the type and the description. It's kinda hacky, but it helps in the generation - * of JSON POJOs with Javadocs. Must be in the format of a list of: - *
-     * <div class="spec-table">
-     *     <div class="spec-row">
-     *     <div class="spec-left"><span class="spec-name">id</span></div>
-     *     <div class="spec-right">
-     *         <div class="spec-type">string<uuid></div>
-     *         <div class="spec-desc">Account ID.</div>
-     *     </div>
-     *     </div>
-     *     ...
-     * </div>
-     * 
- * - * @param alpacaDocXML the alpaca doc xml - */ - public static void printAlpacaHTMLWebDocAsJSONSchema(String alpacaDocXML) - throws ParserConfigurationException, SAXException, IOException, XPathExpressionException { - String nameXPath = "//span[" + xPathClassName("spec-name") + "]"; - String typeXPath = "//div[" + xPathClassName("spec-type") + "]"; - String descriptionXPath = "//div[" + xPathClassName("spec-desc") + "]"; - - System.out.println(getSchemaFromDoc(alpacaDocXML, nameXPath, typeXPath, descriptionXPath)); - } - - /** - * Converts a generic HTML doc for Alpaca with JSON property fields for "name", "type", and "description" into - * schema JSON used for - * jsonSchema2POJO. - * - * @param docHTML the doc html - * @param nameXPath the name x path - * @param typeXPath the type x path - * @param descriptionXPath the description x path - * - * @return the schema from doc - * - * @throws ParserConfigurationException the parser configuration exception - * @throws SAXException the sax exception - * @throws IOException the io exception - * @throws XPathExpressionException the x path expression exception - */ - private static String getSchemaFromDoc(String docHTML, String nameXPath, String typeXPath, String descriptionXPath) - throws ParserConfigurationException, SAXException, IOException, XPathExpressionException { - String schemaJSON = ""; - DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); - Document document = builder.parse(new ByteArrayInputStream(docHTML.getBytes())); - XPathFactory xPathFactory = XPathFactory.newInstance(); - XPath xPath = xPathFactory.newXPath(); - - XPathExpression propertyNamesXPath = xPath.compile(nameXPath); - XPathExpression propertyTypesXPath = xPath.compile(typeXPath); - XPathExpression propertyDescriptionsXPath = xPath.compile(descriptionXPath); - - NodeList propertyNameNodes = (NodeList) propertyNamesXPath.evaluate(document, XPathConstants.NODESET); - NodeList propertyTypesNodes = (NodeList) propertyTypesXPath.evaluate(document, XPathConstants.NODESET); - NodeList propertyDescriptionsNodes = (NodeList) propertyDescriptionsXPath.evaluate(document, - XPathConstants.NODESET); - - for (int propertyIndex = 0; propertyIndex < propertyNameNodes.getLength(); propertyIndex++) { - String propertyName = propertyNameNodes.item(propertyIndex).getTextContent(); - String propertyType = propertyTypesNodes.item(propertyIndex).getTextContent(); - String propertyDescription = propertyDescriptionsNodes.item(propertyIndex).getTextContent(); - - // Property description may be null - if (propertyDescription == null || propertyDescription.isEmpty()) { - propertyDescription = propertyName; - } - - schemaJSON += "\"" + propertyName + "\": {\n"; - schemaJSON += "\"existingJavaType\": \"" + parseDocTypeToSchemaJavaType(propertyType) + "\",\n"; - schemaJSON += "\"title\": \"" + propertyDescription + "\"\n"; - schemaJSON += "},\n"; - } - return schemaJSON; - } - - /** - * @see jsonSchema2Pojo Ref - */ - private static String parseDocTypeToSchemaJavaType(String docType) { - docType = docType.toLowerCase(); - - if (docType.contains("string") || docType.contains("timestamp")) { - return "java.lang.String"; - } else if (docType.contains("boolean") || docType.contains("bool")) { - return "java.lang.Boolean"; - } else if (docType.contains("int") || docType.contains("integer")) { - return "java.lang.Integer"; - } else if (docType.contains("double") || docType.contains("float") || - docType.contains("number")) { - return "java.lang.Double"; - } else { - return "UNKNOWN DOC DATA TYPE"; - } - } - - /** - * @see https://devhints.io/xpath#class-check - */ - private static String xPathClassName(String className) { - return String.format("contains(concat(' ',normalize-space(@class),' '),' %s ')", className); - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/util/format/FormatUtil.java b/src/main/java/net/jacobpeterson/alpaca/util/format/FormatUtil.java deleted file mode 100644 index e797c294..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/util/format/FormatUtil.java +++ /dev/null @@ -1,53 +0,0 @@ -package net.jacobpeterson.alpaca.util.format; - -import java.text.DecimalFormat; -import java.text.DecimalFormatSymbols; -import java.text.NumberFormat; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.temporal.TemporalAccessor; -import java.util.Locale; - -/** - * {@link FormatUtil} is a utility class for various formatting. - */ -public class FormatUtil { - - /** - * The {@link ZoneId} for America/New_York. - */ - public static final ZoneId NEW_YORK_ZONED_ID = ZoneId.of("America/New_York"); - - // Alpaca uses the following rounding mechanics with respect to buy orders: (1) rounded down to two decimal - // places if the last trade price is over $1.00; otherwise, rounded down to four decimal places, hence the '#' in - // the 3rd and 4th least significant decimal digits. - private static final NumberFormat CURRENCY_FORMATTER = new DecimalFormat("#0.00##", - DecimalFormatSymbols.getInstance(Locale.US)); - - /** - * Formats an arbitrary number to a currency format. e.g. $123.45 - * - * @param numberToFormat the {@link Number} to format - * - * @return the formatted string - */ - public static String toCurrencyFormat(Number numberToFormat) { - return CURRENCY_FORMATTER.format(numberToFormat); - } - - /** - * Formats a {@link TemporalAccessor} using {@link DateTimeFormatter#ISO_OFFSET_DATE_TIME}. - *
- * Alpaca requires RFC 3339 formatted timestamps to be provided which includes the timezone. ISO - * 8601 is compatible with RFC 3339 which is what {@link DateTimeFormatter#ISO_OFFSET_DATE_TIME} - * uses. - * - * @param zonedDateTime the {@link ZonedDateTime} - * - * @return the formatted string - */ - public static String toRFC3339Format(ZonedDateTime zonedDateTime) { - return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(zonedDateTime); - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/util/okhttp/JSONBodyBuilder.java b/src/main/java/net/jacobpeterson/alpaca/util/okhttp/JSONBodyBuilder.java deleted file mode 100644 index 11944345..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/util/okhttp/JSONBodyBuilder.java +++ /dev/null @@ -1,65 +0,0 @@ -package net.jacobpeterson.alpaca.util.okhttp; - -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import okhttp3.MediaType; -import okhttp3.RequestBody; - -/** - * {@link JSONBodyBuilder} is builder HTTP bodies. - */ -public class JSONBodyBuilder { - - /** - * The UTF-8 JSON {@link MediaType}. - */ - public static final MediaType JSON_MEDIA_TYPE = MediaType.get("application/json; charset=UTF-8"); - - private String bodyJSON; - private JsonObject bodyJsonObject; - - /** - * Instantiates a new {@link JSONBodyBuilder}. - */ - public JSONBodyBuilder() { - bodyJsonObject = new JsonObject(); - } - - /** - * Instantiates a new {@link JSONBodyBuilder}. - * - * @param bodyJSON a JSON body {@link String} that is used instead of the internal {@link #bodyJsonObject} - */ - public JSONBodyBuilder(String bodyJSON) { - this.bodyJSON = bodyJSON; - } - - /** - * Appends a {@link String} to {@link #bodyJsonObject}. - * - * @param key the key - * @param value the value - */ - public void appendJSONBodyProperty(String key, String value) { - bodyJsonObject.addProperty(key, value); - } - - /** - * Appends a {@link JsonElement} property to {@link #bodyJsonObject}. - * - * @param key the key - * @param value the {@link JsonElement} value - */ - public void appendJSONBodyJSONProperty(String key, JsonElement value) { - bodyJsonObject.add(key, value); - } - - /** - * Builds a {@link RequestBody}. - * - * @return a {@link RequestBody} - */ - public RequestBody build() { - return RequestBody.create(bodyJSON != null ? bodyJSON : bodyJsonObject.toString(), JSON_MEDIA_TYPE); - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/util/properties/PropertyUtil.java b/src/main/java/net/jacobpeterson/alpaca/util/properties/PropertyUtil.java index 50882e47..718d02a3 100644 --- a/src/main/java/net/jacobpeterson/alpaca/util/properties/PropertyUtil.java +++ b/src/main/java/net/jacobpeterson/alpaca/util/properties/PropertyUtil.java @@ -11,9 +11,9 @@ import java.util.Properties; /** - * {@link PropertyUtil} is a util class for all things {@link Properties}. + * {@link PropertyUtil} is a utility class for all {@link Properties}-related handling. */ -public class PropertyUtil { +public final class PropertyUtil { private static final Logger LOGGER = LoggerFactory.getLogger(PropertyUtil.class); @@ -60,15 +60,13 @@ public static String getProperty(String propertyFile, String defaultPropertyFile * @return the string */ public static String getProperty(String propertyFile, String defaultPropertyFile, String key, String defaultValue) { - Properties properties; - + final Properties properties; if (!CACHED_PROPERTIES.containsKey(propertyFile)) { properties = loadPropertyFile(propertyFile, defaultPropertyFile); CACHED_PROPERTIES.put(propertyFile, properties); } else { properties = CACHED_PROPERTIES.get(propertyFile); } - return properties == null ? defaultValue : properties.getProperty(key, defaultValue); } @@ -88,53 +86,53 @@ public synchronized static Properties loadPropertyFile(String propertyFile, Stri // Load the default property file if exists Properties defaultProperties = null; - InputStream defaultPropertyStream = classLoader.getResourceAsStream(defaultPropertyFile); - - if (defaultPropertyStream != null) { - defaultProperties = new Properties(); - - // Load the properties - try { - defaultProperties.load(defaultPropertyStream); - LOGGER.debug("Loaded default properties file: {}", defaultPropertyFile); - } catch (IOException exception) { - LOGGER.error("Could not load default property file: {}\n{}", defaultPropertyFile, exception); - } - - // Close the InputStream - try { - defaultPropertyStream.close(); - } catch (IOException exception) { - LOGGER.error("Could not close default property file stream: {}\n{}", defaultPropertyFile, exception); + if (defaultPropertyFile != null) { + try (InputStream defaultPropertyStream = classLoader.getResourceAsStream(defaultPropertyFile)) { + if (defaultPropertyStream == null) { + LOGGER.warn("No default property file \"{}\" exists on the classpath.", defaultPropertyFile); + } else { + + defaultProperties = new Properties(); + try { + defaultProperties.load(defaultPropertyStream); + LOGGER.debug("Loaded default properties file: {}", defaultPropertyFile); + } catch (IOException exception) { + LOGGER.error("Could not load default property file: {}", defaultPropertyFile, exception); + } + } + } catch (Exception exception) { + LOGGER.error("Could not read default property file stream: {}", defaultPropertyFile, exception); } - } else { - LOGGER.warn("No default property file found for: {}", propertyFile); + LOGGER.warn("No default property file given for: {}", propertyFile); } - // Load the property file + // Load the property file if exists Properties properties = null; - InputStream propertyStream = classLoader.getResourceAsStream(propertyFile); - - if (propertyStream != null) { - // Add default properties if they were found - properties = defaultProperties == null ? new Properties() : new Properties(defaultProperties); - - // Load the properties - try { - properties.load(propertyStream); - LOGGER.info("Loaded properties file: {}", propertyFile); - } catch (IOException exception) { - LOGGER.error("Could not load property file: {}\n{}", propertyFile, exception); + if (propertyFile != null) { + try (InputStream propertyStream = classLoader.getResourceAsStream(propertyFile)) { + if (propertyStream == null) { + LOGGER.warn("No property file \"{}\" exists on the classpath.", propertyFile); + } else { + // Add default properties if they were found + properties = defaultProperties == null ? new Properties() : new Properties(defaultProperties); + + // Load the properties + try { + properties.load(propertyStream); + LOGGER.info("Loaded properties file: {}", propertyFile); + } catch (IOException exception) { + LOGGER.error("Could not load property file: {}", propertyFile, exception); + } + } + } catch (Exception exception) { + LOGGER.error("Could not read property file stream: {}", propertyFile, exception); } + } else if (defaultProperties == null) { + throw new IllegalStateException("No property files were found!"); + } - // Close the InputStream - try { - propertyStream.close(); - } catch (IOException exception) { - LOGGER.error("Could not close property file stream: {}\n{}", propertyFile, exception); - } - } else { + if (properties == null) { LOGGER.debug("Could not find property file: {}", propertyFile); if (defaultProperties != null) { diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java index 18387bcd..59208ccc 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java @@ -1,6 +1,5 @@ package net.jacobpeterson.alpaca.websocket; -import net.jacobpeterson.alpaca.util.okhttp.WebsocketStateListener; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -216,7 +215,7 @@ private void handleReconnectionAttempt() { } /** - * Cleans up this instances state variables. + * Cleans up this instance's state variables. */ protected void cleanupState() { websocket = null; diff --git a/src/main/java/net/jacobpeterson/alpaca/util/okhttp/WebsocketStateListener.java b/src/main/java/net/jacobpeterson/alpaca/websocket/WebsocketStateListener.java similarity index 94% rename from src/main/java/net/jacobpeterson/alpaca/util/okhttp/WebsocketStateListener.java rename to src/main/java/net/jacobpeterson/alpaca/websocket/WebsocketStateListener.java index 38f91d70..4f3713b6 100644 --- a/src/main/java/net/jacobpeterson/alpaca/util/okhttp/WebsocketStateListener.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/WebsocketStateListener.java @@ -1,4 +1,4 @@ -package net.jacobpeterson.alpaca.util.okhttp; +package net.jacobpeterson.alpaca.websocket; import okhttp3.Response; import okhttp3.WebSocket; diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocket.java index 92a15c0c..2fccb63e 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocket.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocket.java @@ -12,6 +12,7 @@ import net.jacobpeterson.alpaca.model.endpoint.streaming.enums.StreamingMessageType; import net.jacobpeterson.alpaca.model.endpoint.streaming.listening.ListeningMessage; import net.jacobpeterson.alpaca.model.endpoint.streaming.trade.TradeUpdateMessage; +import net.jacobpeterson.alpaca.model.properties.EndpointAPIType; import net.jacobpeterson.alpaca.websocket.AlpacaWebsocket; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; @@ -50,14 +51,19 @@ public class StreamingWebsocket extends AlpacaWebsocketalpacaSubdomain
. * - * @param alpacaSubdomain the Alpaca subdomain + * @param endpointAPIType the {@link EndpointAPIType} * * @return a {@link HttpUrl} */ - private static HttpUrl createWebsocketURL(String alpacaSubdomain) { + @SuppressWarnings("UnnecessaryDefault") + private static HttpUrl createWebsocketURL(EndpointAPIType endpointAPIType) { return new HttpUrl.Builder() .scheme("https") // HttpUrl.Builder doesn't recognize "wss" scheme, but "https" works fine - .host(alpacaSubdomain + ".alpaca.markets") + .host((switch (endpointAPIType) { + case LIVE -> "api"; + case PAPER -> "paper-api"; + default -> throw new UnsupportedOperationException(); + }) + ".alpaca.markets") .addPathSegment("stream") .build(); } @@ -68,14 +74,14 @@ private static HttpUrl createWebsocketURL(String alpacaSubdomain) { * Instantiates a new {@link StreamingWebsocket}. * * @param okHttpClient the {@link OkHttpClient} - * @param alpacaSubdomain the Alpaca subdomain + * @param endpointAPIType the {@link EndpointAPIType} * @param keyID the key ID * @param secretKey the secret key * @param oAuthToken the OAuth token */ - public StreamingWebsocket(OkHttpClient okHttpClient, String alpacaSubdomain, + public StreamingWebsocket(OkHttpClient okHttpClient, EndpointAPIType endpointAPIType, String keyID, String secretKey, String oAuthToken) { - super(okHttpClient, createWebsocketURL(alpacaSubdomain), "Streaming", keyID, secretKey, oAuthToken); + super(okHttpClient, createWebsocketURL(endpointAPIType), "Streaming", keyID, secretKey, oAuthToken); listenedStreamMessageTypes = new HashSet<>(); } diff --git a/src/main/resources/alpaca.default.properties b/src/main/resources/alpaca.default.properties index 7da46f1c..e3629268 100644 --- a/src/main/resources/alpaca.default.properties +++ b/src/main/resources/alpaca.default.properties @@ -1,10 +1,2 @@ -#Example: -#key_id = -#secret_key = -#endpoint_api_type = -#data_api_type = -#user_agent = -#Defaults: -endpoint_api_type=paper -data_api_type=iex -user_agent=JavaRuntimeEnvironment/ OkHttp/4.9.1 +endpoint_api_type:paper +data_api_type:iex diff --git a/src/test/java/net/jacobpeterson/alpaca/test/live/AlpacaAPITest.java b/src/test/java/net/jacobpeterson/alpaca/test/live/AlpacaAPITest.java deleted file mode 100644 index 9e6d6136..00000000 --- a/src/test/java/net/jacobpeterson/alpaca/test/live/AlpacaAPITest.java +++ /dev/null @@ -1,289 +0,0 @@ -package net.jacobpeterson.alpaca.test.live; - -import net.jacobpeterson.alpaca.AlpacaAPI; -import net.jacobpeterson.alpaca.model.endpoint.account.Account; -import net.jacobpeterson.alpaca.model.endpoint.accountactivities.AccountActivity; -import net.jacobpeterson.alpaca.model.endpoint.accountactivities.NonTradeActivity; -import net.jacobpeterson.alpaca.model.endpoint.accountactivities.TradeActivity; -import net.jacobpeterson.alpaca.model.endpoint.accountactivities.enums.ActivityType; -import net.jacobpeterson.alpaca.model.endpoint.accountconfiguration.AccountConfiguration; -import net.jacobpeterson.alpaca.model.endpoint.accountconfiguration.enums.DTBPCheck; -import net.jacobpeterson.alpaca.model.endpoint.accountconfiguration.enums.TradeConfirmEmail; -import net.jacobpeterson.alpaca.model.endpoint.clock.Clock; -import net.jacobpeterson.alpaca.model.endpoint.common.enums.SortDirection; -import net.jacobpeterson.alpaca.model.endpoint.orders.Order; -import net.jacobpeterson.alpaca.model.endpoint.orders.enums.CurrentOrderStatus; -import net.jacobpeterson.alpaca.rest.AlpacaClientException; -import net.jacobpeterson.alpaca.rest.endpoint.account.AccountEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.accountconfiguration.AccountConfigurationEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.clock.ClockEndpoint; -import net.jacobpeterson.alpaca.rest.endpoint.orders.OrdersEndpoint; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestMethodOrder; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.time.ZonedDateTime; -import java.util.Collection; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.fail; - -/** - * {@link AlpacaAPITest} tests live endpoints using Alpaca Paper credentials given in the - * alpaca.properties file on the classpath. - */ -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) -public class AlpacaAPITest { - - static { - // Log trace-level - System.setProperty("org.slf4j.simpleLogger.log.net.jacobpeterson", "trace"); - } - - private static final Logger LOGGER = LoggerFactory.getLogger(AlpacaAPITest.class); - private static final int RATE_LIMIT_MILLIS = 200; // Wait 200ms between every test to prevent rate-limiting - - private static AlpacaAPI alpacaAPI; - private static boolean marketOpen; - private static AccountConfiguration accountConfiguration; - - /** - * Executed before all tests in this class. - */ - @BeforeAll - public static void beforeAll() { - alpacaAPI = new AlpacaAPI(); - marketOpen = false; - } - - /** - * Executed before each test. - */ - @BeforeEach - public void beforeEach() { - } - - /** - * Executed after each test. Note that this will {@link Thread#sleep(long)} for {@link #RATE_LIMIT_MILLIS} to - * protect against rate limiting. - */ - @AfterEach - public void afterEach() { - try { - Thread.sleep(RATE_LIMIT_MILLIS); - } catch (InterruptedException exception) { - fail("Interrupted when tearing down!"); - } - } - - /** - * Tests {@link ClockEndpoint#get()}. - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - @Test - @org.junit.jupiter.api.Order(1) - public void testClockEndpointGet() throws AlpacaClientException { - Clock clock = alpacaAPI.clock().get(); - assertNotNull(clock); - - LOGGER.debug("{}", clock); - - assertNotNull(clock.getTimestamp()); - assertNotNull(clock.getIsOpen()); - assertNotNull(clock.getNextOpen()); - assertNotNull(clock.getNextClose()); - - marketOpen = clock.getIsOpen(); - if (marketOpen) { - LOGGER.info("Market is currently open! All live endpoints will be tested."); - } else { - LOGGER.info("Market is currently closed! Only some live endpoints will be tested."); - } - } - - /** - * Tests {@link AccountEndpoint#get()}. - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - * @throws NumberFormatException thrown for {@link NumberFormatException}s - */ - @Test - @SuppressWarnings("ResultOfMethodCallIgnored") - public void testAccountEndpointGet() throws AlpacaClientException, NumberFormatException { - Account account = alpacaAPI.account().get(); - assertNotNull(account); - - LOGGER.debug("{}", account); - - // Assert basic data integrity and not null - Double.parseDouble(account.getCash()); - Double.parseDouble(account.getPortfolioValue()); - Double.parseDouble(account.getLongMarketValue()); - Double.parseDouble(account.getShortMarketValue()); - Double.parseDouble(account.getEquity()); - Double.parseDouble(account.getLastEquity()); - Double.parseDouble(account.getBuyingPower()); - Double.parseDouble(account.getInitialMargin()); - Double.parseDouble(account.getMaintenanceMargin()); - Double.parseDouble(account.getLastMaintenanceMargin()); - Double.parseDouble(account.getDaytradingBuyingPower()); - Double.parseDouble(account.getRegtBuyingPower()); - - // Assert other data exists - assertNotNull(account.getId()); - assertNotNull(account.getAccountNumber()); - assertNotNull(account.getStatus()); - assertNotNull(account.getCurrency()); - assertNotNull(account.getPatternDayTrader()); - assertNotNull(account.getTradeSuspendedByUser()); - assertNotNull(account.getTradingBlocked()); - assertNotNull(account.getTransfersBlocked()); - assertNotNull(account.getAccountBlocked()); - assertNotNull(account.getCreatedAt()); - assertNotNull(account.getShortingEnabled()); - assertNotNull(account.getMultiplier()); - assertNotNull(account.getSma()); - assertNotNull(account.getDaytradeCount()); - } - - /** - * Tests - * @{@link AccountActivitiesEndpoint#get(ZonedDateTime, ZonedDateTime, ZonedDateTime, SortDirection, Integer, - *String, ActivityType...)} one {@link AccountActivity} exists until now. - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - @Test - public void testAccountActivitiesEndpointGetOneActivityExistsUntilNow() throws AlpacaClientException { - List accountActivities = alpacaAPI.accountActivities().get( - null, - ZonedDateTime.now(), - null, - SortDirection.ASCENDING, - 1, - null, - (ActivityType[]) null); - assertNotNull(accountActivities); - assertFalse(accountActivities.isEmpty()); - - accountActivities.forEach(accountActivity -> LOGGER.debug(accountActivity.toString())); - - AccountActivity accountActivity = accountActivities.get(0); - if (accountActivity instanceof TradeActivity) { - TradeActivity tradeActivity = (TradeActivity) accountActivity; - assertNotNull(tradeActivity.getActivityType()); - assertNotNull(tradeActivity.getId()); - assertNotNull(tradeActivity.getCumulativeQuantity()); - assertNotNull(tradeActivity.getRemainingQuantity()); - assertNotNull(tradeActivity.getPrice()); - assertNotNull(tradeActivity.getQuantity()); - assertNotNull(tradeActivity.getSide()); - assertNotNull(tradeActivity.getSymbol()); - assertNotNull(tradeActivity.getTransactionTime()); - assertNotNull(tradeActivity.getOrderId()); - assertNotNull(tradeActivity.getType()); - } else if (accountActivity instanceof NonTradeActivity) { - NonTradeActivity nonTradeActivity = (NonTradeActivity) accountActivity; - assertNotNull(nonTradeActivity.getActivityType()); - assertNotNull(nonTradeActivity.getId()); - assertNotNull(nonTradeActivity.getDate()); - assertNotNull(nonTradeActivity.getNetAmount()); - assertNotNull(nonTradeActivity.getSymbol()); - assertNotNull(nonTradeActivity.getQuantity()); - assertNotNull(nonTradeActivity.getPerShareAmount()); - assertNotNull(nonTradeActivity.getDescription()); - } - } - - /** - * Test {@link AccountConfigurationEndpoint#get()}. - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - @Test - @org.junit.jupiter.api.Order(1) - public void testAccountConfigurationEndpointGet() throws AlpacaClientException { - AccountConfiguration accountConfiguration = alpacaAPI.accountConfiguration().get(); - assertNotNull(accountConfiguration); - - LOGGER.debug("{}", accountConfiguration); - - assertNotNull(accountConfiguration.getDtbpCheck()); - assertNotNull(accountConfiguration.getTradeConfirmEmail()); - assertNotNull(accountConfiguration.getSuspendTrade()); - assertNotNull(accountConfiguration.getNoShorting()); - - AlpacaAPITest.accountConfiguration = accountConfiguration; - } - - /** - * Test {@link AccountConfigurationEndpoint#set(AccountConfiguration)}. - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - @Test - @org.junit.jupiter.api.Order(2) - public void testAccountConfigurationEndpointSet() throws AlpacaClientException { - if (accountConfiguration == null) { - AccountConfiguration newAccountConfiguration = new AccountConfiguration( - DTBPCheck.BOTH, - TradeConfirmEmail.NONE, - false, - false); - LOGGER.info("Settings Account Configuration to: {}", newAccountConfiguration); - alpacaAPI.accountConfiguration().set(newAccountConfiguration); - } else { - alpacaAPI.accountConfiguration().set(accountConfiguration); - } - } - - /** - * Test - * {@link OrdersEndpoint#get(CurrentOrderStatus, Integer, ZonedDateTime, ZonedDateTime, SortDirection, Boolean, - * Collection)} one {@link Order} exists until now. - * - * @throws AlpacaClientException thrown for {@link AlpacaClientException}s - */ - @Test - public void testOrdersEndpointGetOneOrderExistsUntilNow() throws AlpacaClientException { - List orders = alpacaAPI.orders().get( - CurrentOrderStatus.ALL, - 1, - null, - ZonedDateTime.now(), - SortDirection.ASCENDING, - true, - null); - - assertNotNull(orders); - assertFalse(orders.isEmpty()); - - orders.forEach(order -> LOGGER.debug("{}", order)); - - // Assert required fields are present - Order order = orders.get(0); - assertNotNull(order.getId()); - assertNotNull(order.getClientOrderId()); - assertNotNull(order.getCreatedAt()); - assertNotNull(order.getUpdatedAt()); - assertNotNull(order.getSubmittedAt()); - assertNotNull(order.getAssetId()); - assertNotNull(order.getSymbol()); - assertNotNull(order.getAssetClass()); - assertNotNull(order.getQuantity()); - assertNotNull(order.getFilledQuantity()); - assertNotNull(order.getType()); - assertNotNull(order.getSide()); - assertNotNull(order.getTimeInForce()); - assertNotNull(order.getStatus()); - assertNotNull(order.getExtendedHours()); - } -} diff --git a/src/test/java/net/jacobpeterson/alpaca/test/mock/AlpacaAPITest.java b/src/test/java/net/jacobpeterson/alpaca/test/mock/AlpacaAPITest.java deleted file mode 100644 index d5f4f320..00000000 --- a/src/test/java/net/jacobpeterson/alpaca/test/mock/AlpacaAPITest.java +++ /dev/null @@ -1,74 +0,0 @@ -package net.jacobpeterson.alpaca.test.mock; - -import net.jacobpeterson.alpaca.AlpacaAPI; -import net.jacobpeterson.alpaca.model.properties.DataAPIType; -import net.jacobpeterson.alpaca.model.properties.EndpointAPIType; -import okhttp3.OkHttpClient; -import org.junit.jupiter.api.Test; - -/** - * {@link AlpacaAPITest} tests {@link AlpacaAPI} using mocked objects with Mockito. - */ -public class AlpacaAPITest { - - static { - System.setProperty("org.slf4j.simpleLogger.log.net.jacobpeterson.alpaca", "trace"); - } - - /** - * Tests {@link AlpacaAPI#AlpacaAPI()}. - */ - @Test - public void testAlpacaAPIConstructor_Default() { - new AlpacaAPI(); - } - - /** - * Tests {@link AlpacaAPI#AlpacaAPI(String, String)}. - */ - @Test - public void testAlpacaAPIConstructor_keyID_secret() { - String keyID = "ABCDEFGHIJKLM"; - String secret = "NOPQURSTUVWXYZ"; - - new AlpacaAPI(keyID, secret); - } - - /** - * Tests {@link AlpacaAPI#AlpacaAPI(String, String, EndpointAPIType, DataAPIType)}. - */ - @Test - public void testAlpacaAPIConstructor_keyID_secret_endpointAPIType_dataAPIType() { - String keyID = "ABCDEFGHIJKLM"; - String secret = "NOPQURSTUVWXYZ"; - - new AlpacaAPI(keyID, secret, EndpointAPIType.PAPER, DataAPIType.IEX); - new AlpacaAPI(keyID, secret, EndpointAPIType.LIVE, DataAPIType.SIP); - } - - /** - * Tests {@link AlpacaAPI#AlpacaAPI(String)}. - */ - @Test - public void testAlpacaAPIConstructor_oAuthToken() { - String oAuthToken = "ABCDEFGHIJKLMNOPQURSTUVWXYZ"; - - new AlpacaAPI(oAuthToken); - } - - /** - * Tests {@link AlpacaAPI#AlpacaAPI(OkHttpClient, String, String, String, EndpointAPIType, DataAPIType)}. - */ - @Test - public void testAlpacaAPIConstructor_okHttpClient_keyID_secret_oAuthToken_endpointAPIType_dataAPIType() { - OkHttpClient okHttpClient = new OkHttpClient(); - String keyID = "ABCDEFGHIJKLM"; - String secret = "NOPQURSTUVWXYZ"; - String oAuthToken = "ABCDEFGHIJKLMNOPQURSTUVWXYZ"; - - new AlpacaAPI(okHttpClient, null, null, oAuthToken, EndpointAPIType.PAPER, DataAPIType.IEX); - new AlpacaAPI(okHttpClient, keyID, secret, null, EndpointAPIType.LIVE, DataAPIType.SIP); - new AlpacaAPI(okHttpClient, null, null, oAuthToken, EndpointAPIType.PAPER, DataAPIType.IEX); - new AlpacaAPI(okHttpClient, keyID, secret, null, EndpointAPIType.LIVE, DataAPIType.SIP); - } -} diff --git a/src/test/resources/alpaca.default.properties b/src/test/resources/alpaca.default.properties deleted file mode 100644 index 58a3702f..00000000 --- a/src/test/resources/alpaca.default.properties +++ /dev/null @@ -1,5 +0,0 @@ -key_id=ABCDEFGHIJKLM -secret_key=NOPQURSTUVWXYZ -endpoint_api_type=paper -data_api_type=iex -user_agent=JavaRuntimeEnvironment/ OkHttp/4.9.1 From 0383b926bd61028ad396e510ea319d47fa2fc91b Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Sat, 2 Mar 2024 13:51:46 -0800 Subject: [PATCH 56/84] Fix JSONSchema2POJOAdaptedPlugin --- .../GeneratePOJOsTask.groovy | 113 ++++++++---------- .../JSONSchema2POJOAdaptedPlugin.groovy | 4 +- 2 files changed, 55 insertions(+), 62 deletions(-) diff --git a/buildSrc/src/main/groovy/net/jacobpeterson/alpaca/buildsrc/plugin/jsonschema2pojoadapted/GeneratePOJOsTask.groovy b/buildSrc/src/main/groovy/net/jacobpeterson/alpaca/buildsrc/plugin/jsonschema2pojoadapted/GeneratePOJOsTask.groovy index 88ac5a4a..650a7c7b 100644 --- a/buildSrc/src/main/groovy/net/jacobpeterson/alpaca/buildsrc/plugin/jsonschema2pojoadapted/GeneratePOJOsTask.groovy +++ b/buildSrc/src/main/groovy/net/jacobpeterson/alpaca/buildsrc/plugin/jsonschema2pojoadapted/GeneratePOJOsTask.groovy @@ -19,7 +19,6 @@ import static com.google.common.base.Preconditions.checkArgument /** * {@link GeneratePOJOsTask} is a Gradle task that invokes jsonschema2pojo with special configurations * and generation logic. - * TODO publish this plugin somewhere so the code doesn't have to be copied between 'cli' and 'system' Gradle projects. * @see GenerateJsonSchemaJavaTask */ class GeneratePOJOsTask extends DefaultTask { @@ -39,69 +38,63 @@ class GeneratePOJOsTask extends DefaultTask { group = "Build" description = "Generates Java POJOs from JSON schemas." - project.afterEvaluate { - // Validate plugin configuration - configuration = project.extensions.getByName(EXTENSION_KEY) as GenerationConfig - checkArgument(!configuration.source.hasNext(), "'source' should not be set for '${name}'.") - checkArgument(configuration.targetDirectory == null, "'targetDirectory' should not be set for '${name}'.") - - // Set custom defaults on plugin configuration - configuration.targetDirectory = project.file( - "${project.layout.buildDirectory.getAsFile().get().getPath()}/generated-sources/jsonschema2pojo") - configuration.targetPackage = configuration.targetPackage == "" ? - "${project.group}.model" : configuration.targetPackage - configuration.propertyWordDelimiters = ["-", "_"] as char[] - configuration.annotationStyle = "gson" - configuration.sourceType = "jsonschema" - configuration.customDateTimePattern = "yyyy-MM-ddTHH:mm:ssZ" - configuration.includeConstructors = true - configuration.serializable = true - configuration.includeGetters = true - configuration.includeSetters = true - configuration.includeCopyConstructor = true - configuration.generateBuilders = true - - // Get project main java source set - final def projectMainJavaSourceSet = - project.extensions.getByType(SourceSetContainer).named("main").get().java - - // Set task dependents and dependencies - project.tasks.named("compileJava").get().dependsOn(this) - dependsOn(project.tasks.named("processResources").get()) - - // Walk through source files to create a list of JSON schema files to process - jsonSchemaFileConfigs = new ArrayList<>() - projectMainJavaSourceSet.srcDirs.each { srcDir -> - if (!srcDir.exists()) { - return - } - final def sourceDirPath = srcDir.getAbsolutePath() - srcDir.eachFileRecurse { sourceFile -> - final def sourceFilePath = sourceFile.getAbsolutePath() - if (sourceFilePath.endsWith(".json")) { - def targetPackage = sourceFile.getParentFile().getAbsolutePath() - .substring(sourceDirPath.length()) - .replace(File.separator, ".").replace("-", "").replace("_", "") - .toLowerCase() - targetPackage = targetPackage.replace("${project.group}", "") - targetPackage = targetPackage.startsWith(".") ? targetPackage.substring(1) : targetPackage - targetPackage = configuration.targetPackage + - (targetPackage.isEmpty() ? "" : targetPackage) - jsonSchemaFileConfigs.add(new JSONSchemaFileConfig(sourceFile, targetPackage)) - - inputs.file(sourceFile) - outputs.file(project.file(configuration.targetDirectory.getAbsolutePath() + File.separator + - targetPackage.replace(".", File.separator) + File.separator + - sourceFile.getName().replace(".json", ".java"))) - } + // Validate plugin configuration + configuration = project.extensions.getByName(EXTENSION_KEY) as GenerationConfig + checkArgument(!configuration.source.hasNext(), "'source' should not be set for '${name}'.") + checkArgument(configuration.targetDirectory == null, "'targetDirectory' should not be set for '${name}'.") + + // Set custom defaults on plugin configuration + configuration.targetDirectory = project.file( + "${project.layout.buildDirectory.getAsFile().get().getPath()}/generated-sources/jsonschema2pojo") + configuration.targetPackage = configuration.targetPackage == "" ? + "${project.group}.model" : configuration.targetPackage + configuration.propertyWordDelimiters = ["-", "_"] as char[] + configuration.annotationStyle = "gson" + configuration.sourceType = "jsonschema" + configuration.customDateTimePattern = "yyyy-MM-ddTHH:mm:ssZ" + configuration.includeConstructors = true + configuration.serializable = true + configuration.includeGetters = true + configuration.includeSetters = true + configuration.includeCopyConstructor = true + configuration.generateBuilders = true + + // Get project main java source set + final def projectMainJavaSourceSet = + project.extensions.getByType(SourceSetContainer).named("main").get().java + + // Walk through source files to create a list of JSON schema files to process + jsonSchemaFileConfigs = new ArrayList<>() + projectMainJavaSourceSet.srcDirs.each { srcDir -> + if (!srcDir.exists()) { + return + } + final def sourceDirPath = srcDir.getAbsolutePath() + srcDir.eachFileRecurse { sourceFile -> + final def sourceFilePath = sourceFile.getAbsolutePath() + if (sourceFilePath.endsWith(".json")) { + def targetPackage = sourceFile.getParentFile().getAbsolutePath() + .substring(sourceDirPath.length()) + .replace(File.separator, ".").replace("-", "").replace("_", "") + .toLowerCase() + targetPackage = targetPackage.replace("${project.group}", "") + targetPackage = targetPackage.startsWith(".") ? targetPackage.substring(1) : targetPackage + targetPackage = configuration.targetPackage + + (targetPackage.isEmpty() ? "" : targetPackage) + jsonSchemaFileConfigs.add(new JSONSchemaFileConfig(sourceFile, targetPackage)) + + inputs.file(sourceFile) + outputs.file(project.file(configuration.targetDirectory.getAbsolutePath() + File.separator + + targetPackage.replace(".", File.separator) + File.separator + + sourceFile.getName().replace(".json", ".java"))) } } + } - outputs.dir(configuration.targetDirectory) - outputs.cacheIf { true } + outputs.dir(configuration.targetDirectory) + outputs.cacheIf { true } - projectMainJavaSourceSet.srcDirs(configuration.targetDirectory) - } + projectMainJavaSourceSet.srcDirs(configuration.targetDirectory) } @TaskAction diff --git a/buildSrc/src/main/groovy/net/jacobpeterson/alpaca/buildsrc/plugin/jsonschema2pojoadapted/JSONSchema2POJOAdaptedPlugin.groovy b/buildSrc/src/main/groovy/net/jacobpeterson/alpaca/buildsrc/plugin/jsonschema2pojoadapted/JSONSchema2POJOAdaptedPlugin.groovy index 9d53a826..963e8276 100644 --- a/buildSrc/src/main/groovy/net/jacobpeterson/alpaca/buildsrc/plugin/jsonschema2pojoadapted/JSONSchema2POJOAdaptedPlugin.groovy +++ b/buildSrc/src/main/groovy/net/jacobpeterson/alpaca/buildsrc/plugin/jsonschema2pojoadapted/JSONSchema2POJOAdaptedPlugin.groovy @@ -17,8 +17,8 @@ class JSONSchema2POJOAdaptedPlugin implements Plugin { void apply(Project project) { project.extensions.create(EXTENSION_KEY, JsonSchemaExtension) if (project.plugins.hasPlugin("java")) { - final def generatePOJOsTask = project.tasks.register("generatePOJOs", GeneratePOJOsTask) - generatePOJOsTask.get().enabled = true + project.tasks.register("generatePOJOs", GeneratePOJOsTask) + project.tasks.compileJava.dependsOn "generatePOJOs" } else { throw new GradleException("'java' Gradle plugin required.") } From 73a9a0bfcaed285fbe1bc6538cb60ab1d5236569 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Sat, 2 Mar 2024 13:52:37 -0800 Subject: [PATCH 57/84] WIP update `build.gradle` to modify OpenAPI clients --- build.gradle | 49 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/build.gradle b/build.gradle index 844d41b9..bb8bd333 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,7 @@ import org.openapitools.generator.gradle.plugin.tasks.GenerateTask +import java.nio.file.Paths + plugins { id "java" id "java-library" @@ -62,14 +64,16 @@ java { withJavadocJar() withSourcesJar() } +javadocJar.dependsOn generatePOJOs +sourcesJar.dependsOn generatePOJOs [compileJava, compileTestJava]*.options*.encoding = "UTF-8" javadoc { options.addStringOption("source", "17") - options.addStringOption("Xdoclint:none", "-quiet") // Suppress Javadoc linting warnings options.addStringOption("charset", "UTF-8") - options.addStringOption("link", "https://docs.oracle.com/javase/8/docs/api/") + options.addStringOption("Xdoclint:none", "-quiet") // Suppress Javadoc linting warnings + options.addStringOption("link", "https://docs.oracle.com/en/java/javase/17/docs/api/") } // @@ -86,12 +90,15 @@ final File specDownloadPath = new File(project.layout.buildDirectory.get().getAs final File generatedClientLibrariesPath = new File(project.layout.buildDirectory.get().getAsFile(), "/generated/openapi/") -final def generateAllOpenAPIClientsTasks = tasks.register("generateAllOpenAPIClients") { +final def generateOpenAPIClientsTasks = tasks.register("generateOpenAPIClients") { doLast { - fixOpenAPIGeneratedClientIssues(generatedClientLibrariesPath) + fixOpenAPIGeneratedClientIssues(specIDsOfFileNames, generatedClientLibrariesPath) } } -compileJava.dependsOn generateAllOpenAPIClientsTasks +compileJava.dependsOn generateOpenAPIClientsTasks +jar.dependsOn generateOpenAPIClientsTasks +javadocJar.dependsOn generateOpenAPIClientsTasks +sourcesJar.dependsOn generateOpenAPIClientsTasks def downloadOpenAPISpecFilesTask = tasks.register("downloadOpenAPISpecFiles") { configure { @@ -165,7 +172,9 @@ for (def specFileName : specIDsOfFileNames.keySet()) { "apiPackage", destinationPackage + ".api", "modelPackage", destinationPackage + ".model", "library", "okhttp-gson", - "documentationProvider", "none")) + "documentationProvider", "none", + "useBeanValidation", "false", + "performBeanValidation", "false")) cleanupOutput.set(true) validateSpec.set(false) skipValidateSpec.set(true) @@ -174,22 +183,41 @@ for (def specFileName : specIDsOfFileNames.keySet()) { generateApiTests.set(false) generateApiDocumentation.set(false) } - generateAllOpenAPIClientsTasks.configure { + generateOpenAPIClientsTasks.configure { dependsOn generateTask } } -static void fixOpenAPIGeneratedClientIssues(File generatedClientLibrariesPath) { +static void fixOpenAPIGeneratedClientIssues(Map specIDsOfFileNames, File generatedClientLibrariesPath) { + // TODO remove these manual fixes once OpenAPI fixes these issues + + final def sourceFilesPath = "src/main/java/net/jacobpeterson/alpaca/openapi" + // Fix broker spec generated client issues with discriminators // on some enums: https://github.com/OpenAPITools/openapi-generator/issues/806 final def brokerTransferDataJavaFile = new File(generatedClientLibrariesPath, - "broker/src/main/java/net/jacobpeterson/alpaca/openapi/broker/model/TransferData.java") + "broker/${sourceFilesPath}/broker/model/TransferData.java") brokerTransferDataJavaFile.text = brokerTransferDataJavaFile.text .replace("this.transferType = this.getClass().getSimpleName();", "") final def brokerTransferResourceJavaFile = new File(generatedClientLibrariesPath, - "broker/src/main/java/net/jacobpeterson/alpaca/openapi/broker/model/TransferResource.java") + "broker/${sourceFilesPath}/broker/model/TransferResource.java") brokerTransferResourceJavaFile.text = brokerTransferResourceJavaFile.text .replace("this.type = this.getClass().getSimpleName();", "") + + // Remove the JSON validation that the generated client models always run (there isn't an option to + // disable it for some reason) + specIDsOfFileNames.keySet().stream().map { it.replace(".json", "") }.forEach { + for (File modelFile : Paths.get(generatedClientLibrariesPath.getPath(), + it, sourceFilesPath, it.replace("-", ""), "model").toFile().listFiles()) { + modelFile.text = modelFile.text + .replaceAll("/\\*\\*\\n {2}\\* Validates the JSON Element and throws an exception if issues found" + + "[\\s\\S]*?\\*/", "") + .replace("public static void validateJsonElement(JsonElement jsonElement) throws IOException {", + "public static boolean validate = false;\n public static void " + + "validateJsonElement(JsonElement jsonElement) throws IOException {\n" + + " if (!validate) return;") + } + } } // @@ -206,6 +234,7 @@ publishing { groupId = projectGroup artifactId = projectArtifactID version = projectVersion + from(components["java"]) pom { name = projectArtifactID From a4a9e650cced0bc44c61f42bd1718f16c79f5918 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Sat, 2 Mar 2024 21:27:45 -0800 Subject: [PATCH 58/84] WIP implement wrapper classes for OpenAPI generate clients --- build.gradle | 17 +- .../net/jacobpeterson/alpaca/AlpacaAPI.java | 291 ++++++++----- .../jacobpeterson/alpaca/AlpacaBrokerAPI.java | 304 ++++++++++++++ .../alpaca/AlpacaMarketDataAPI.java | 152 +++++++ .../jacobpeterson/alpaca/AlpacaTraderAPI.java | 212 ++++++++++ .../alpaca/properties/AlpacaProperties.java | 29 -- .../alpaca/properties/data_api_type.json | 9 - .../alpaca/properties/endpoint_api_type.json | 9 - .../alpaca/util/apikey/APIKeyUtil.java | 23 ++ .../apitype/broker_api_endpoint_type.json | 8 + .../apitype/market_data_api_source_type.json | 8 + .../apitype/trader_api_endpoint_type.json | 8 + .../alpaca/util/gson/GsonUtil.java | 28 -- .../util/gson/adapters/LocalDateAdapter.java | 31 -- .../util/gson/adapters/LocalTimeAdapter.java | 31 -- .../gson/adapters/ZonedDateTimeAdapter.java | 31 -- .../alpaca/util/properties/PropertyUtil.java | 146 ------- .../alpaca/websocket/AlpacaWebsocket.java | 292 ------------- .../websocket/AlpacaWebsocketInterface.java | 81 ---- .../AlpacaWebsocketMessageListener.java | 20 - .../websocket/WebsocketStateListener.java | 32 -- .../marketdata/MarketDataListener.java | 11 - .../marketdata/MarketDataWebsocket.java | 387 ------------------ .../MarketDataWebsocketInterface.java | 86 ---- .../crypto/CryptoMarketDataWebsocket.java | 27 -- .../news/NewsMarketDataWebsocket.java | 15 - .../stock/StockMarketDataWebsocket.java | 30 -- .../streaming/StreamingListener.java | 11 - .../streaming/StreamingWebsocket.java | 257 ------------ .../StreamingWebsocketInterface.java | 26 -- src/main/resources/alpaca.default.properties | 2 - 31 files changed, 914 insertions(+), 1700 deletions(-) create mode 100644 src/main/java/net/jacobpeterson/alpaca/AlpacaBrokerAPI.java create mode 100644 src/main/java/net/jacobpeterson/alpaca/AlpacaMarketDataAPI.java create mode 100644 src/main/java/net/jacobpeterson/alpaca/AlpacaTraderAPI.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/properties/AlpacaProperties.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/properties/data_api_type.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/properties/endpoint_api_type.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/util/apikey/APIKeyUtil.java create mode 100644 src/main/java/net/jacobpeterson/alpaca/util/apitype/broker_api_endpoint_type.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/util/apitype/market_data_api_source_type.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/util/apitype/trader_api_endpoint_type.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/util/gson/GsonUtil.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/util/gson/adapters/LocalDateAdapter.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/util/gson/adapters/LocalTimeAdapter.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/util/gson/adapters/ZonedDateTimeAdapter.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/util/properties/PropertyUtil.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketInterface.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketMessageListener.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/WebsocketStateListener.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataListener.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocketInterface.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/crypto/CryptoMarketDataWebsocket.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/news/NewsMarketDataWebsocket.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/stock/StockMarketDataWebsocket.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingListener.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocket.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocketInterface.java delete mode 100644 src/main/resources/alpaca.default.properties diff --git a/build.gradle b/build.gradle index bb8bd333..b13ea565 100644 --- a/build.gradle +++ b/build.gradle @@ -43,6 +43,9 @@ dependencies { // GSON implementation group: "com.google.code.gson", name: "gson", version: "2.10.1" + // Jetbrains Annotations + implementation group: "org.jetbrains", name: "annotations", version: "24.1.0" + // OkHttp implementation group: "com.squareup.okhttp3", name: "okhttp", version: "5.0.0-alpha.12" implementation group: "com.squareup.okhttp3", name: "logging-interceptor", version: "5.0.0-alpha.12" @@ -213,11 +216,21 @@ static void fixOpenAPIGeneratedClientIssues(Map specIDsOfFileNam .replaceAll("/\\*\\*\\n {2}\\* Validates the JSON Element and throws an exception if issues found" + "[\\s\\S]*?\\*/", "") .replace("public static void validateJsonElement(JsonElement jsonElement) throws IOException {", - "public static boolean validate = false;\n public static void " + + "public static boolean validate = false;\n public static void " + "validateJsonElement(JsonElement jsonElement) throws IOException {\n" + - " if (!validate) return;") + " if (!validate) return;") } } + + // Remove the necessary authentication header logic from the generate client's ApiClient + specIDsOfFileNames.keySet().stream().map { it.replace(".json", "") }.forEach { + final def apiClientFile = Paths.get(generatedClientLibrariesPath.getPath(), it, sourceFilesPath, + it.replace("-", ""), "ApiClient.java").toFile() + apiClientFile.text = apiClientFile.text + .replaceAll("// Setup authentications \\(key: authentication name, value: authentication\\)" + + "[\\s\\S]*?authentications\\);", "") + .replaceAll("for \\(String authName : authNames\\)[\\s\\S]*?uri\\);\\n. {7}}", "") + } } // diff --git a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java index bcaaec25..577324ec 100644 --- a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java +++ b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java @@ -1,25 +1,24 @@ package net.jacobpeterson.alpaca; -import net.jacobpeterson.alpaca.model.properties.DataAPIType; -import net.jacobpeterson.alpaca.model.properties.EndpointAPIType; -import net.jacobpeterson.alpaca.properties.AlpacaProperties; -import net.jacobpeterson.alpaca.websocket.marketdata.MarketDataWebsocketInterface; -import net.jacobpeterson.alpaca.websocket.marketdata.crypto.CryptoMarketDataWebsocket; -import net.jacobpeterson.alpaca.websocket.marketdata.news.NewsMarketDataWebsocket; -import net.jacobpeterson.alpaca.websocket.marketdata.stock.StockMarketDataWebsocket; -import net.jacobpeterson.alpaca.websocket.streaming.StreamingWebsocket; -import net.jacobpeterson.alpaca.websocket.streaming.StreamingWebsocketInterface; +import net.jacobpeterson.alpaca.model.util.apitype.BrokerAPIEndpointType; +import net.jacobpeterson.alpaca.model.util.apitype.MarketDataAPISourceType; +import net.jacobpeterson.alpaca.model.util.apitype.TraderAPIEndpointType; import okhttp3.OkHttpClient; import okhttp3.logging.HttpLoggingInterceptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; +import static net.jacobpeterson.alpaca.model.util.apitype.BrokerAPIEndpointType.SANDBOX; +import static net.jacobpeterson.alpaca.model.util.apitype.MarketDataAPISourceType.IEX; +import static net.jacobpeterson.alpaca.model.util.apitype.TraderAPIEndpointType.PAPER; /** - * {@link AlpacaAPI} is the main class used to interface with the various Alpaca API endpoints. You will generally only - * need one instance of this class in your application. + * {@link AlpacaAPI} is the main class used to interface with the various Alpaca API endpoints. If you are using the + * Trading or Market Data APIs for a single Alpaca account or if you are using the Broker API, you will generally only + * need one instance of this class. However, if you are using the Trading API with OAuth to act on behalf of an Alpaca + * account, this class is optimized so that it can be instantiated quickly, especially when an existing + * {@link OkHttpClient} is given in the constructor. Additionally, all API endpoint instances are instantiated lazily. + * This class is thread-safe. * * @see Alpaca Docs */ @@ -27,134 +26,186 @@ public class AlpacaAPI { private static final Logger LOGGER = LoggerFactory.getLogger(AlpacaAPI.class); + private final String traderKeyID; + private final String traderSecretKey; + private final String traderOAuthToken; + private final TraderAPIEndpointType traderAPIEndpointType; + private final MarketDataAPISourceType marketDataAPISourceType; + private final String brokerAPIKey; + private final String brokerAPISecret; + private final BrokerAPIEndpointType brokerAPIEndpointType; private final OkHttpClient okHttpClient; - private final StreamingWebsocket streamingWebsocket; - private final CryptoMarketDataWebsocket cryptoMarketDataWebsocket; - private final StockMarketDataWebsocket stockMarketDataWebsocket; - private final NewsMarketDataWebsocket newsMarketDataWebsocket; + + private AlpacaTraderAPI trader; + private AlpacaMarketDataAPI marketData; + private AlpacaBrokerAPI broker; /** - * Instantiates a new {@link AlpacaAPI} using properties specified in alpaca.properties file (or their - * associated defaults). + * Instantiates a new {@link AlpacaAPI}. Use this constructor if you are using the Trading or Market Data APIs for a + * single Alpaca account. + * + * @param traderKeyID the Trader key ID + * @param traderSecretKey the Trader secret key + * @param traderAPIEndpointType the {@link TraderAPIEndpointType} + * @param marketDataAPISourceType the {@link MarketDataAPISourceType} */ - public AlpacaAPI() { - this(AlpacaProperties.KEY_ID, - AlpacaProperties.SECRET_KEY, - AlpacaProperties.ENDPOINT_API_TYPE, - AlpacaProperties.DATA_API_TYPE); + public AlpacaAPI(String traderKeyID, String traderSecretKey, TraderAPIEndpointType traderAPIEndpointType, + MarketDataAPISourceType marketDataAPISourceType) { + this(traderKeyID, traderSecretKey, null, traderAPIEndpointType, marketDataAPISourceType, null, null, null, + null); } /** - * Instantiates a new {@link AlpacaAPI} using properties specified in the given {@link Builder}, otherwise from - * alpaca.properties file (or their associated defaults). + * Instantiates a new {@link AlpacaAPI}. Use this constructor if you are using the Trading or Market Data APIs for a + * single Alpaca account and a custom {@link OkHttpClient} instance. + * + * @param traderKeyID the Trader key ID + * @param traderSecretKey the Trader secret key + * @param traderAPIEndpointType the {@link TraderAPIEndpointType} + * @param marketDataAPISourceType the {@link MarketDataAPISourceType} + * @param okHttpClient an existing {@link OkHttpClient} or null to create a new default + * instance */ - private AlpacaAPI(Builder builder) { - this(builder.keyID, - builder.secretKey, - builder.endpointAPIType, - builder.dataAPIType); + public AlpacaAPI(String traderKeyID, String traderSecretKey, TraderAPIEndpointType traderAPIEndpointType, + MarketDataAPISourceType marketDataAPISourceType, OkHttpClient okHttpClient) { + this(traderKeyID, traderSecretKey, null, traderAPIEndpointType, marketDataAPISourceType, null, null, null, + okHttpClient); } /** - * Instantiates a new {@link AlpacaAPI}. + * Instantiates a new {@link AlpacaAPI}. Use this constructor if you are using the Trading API with OAuth to act on + * behalf of an Alpaca account. * - * @param keyID the key ID - * @param secretKey the secret key + * @param traderOAuthToken the Trader OAuth token + * @param traderAPIEndpointType the {@link TraderAPIEndpointType} */ - public AlpacaAPI(String keyID, String secretKey) { - this(null, keyID, secretKey, null, - AlpacaProperties.ENDPOINT_API_TYPE, - AlpacaProperties.DATA_API_TYPE); + public AlpacaAPI(String traderOAuthToken, TraderAPIEndpointType traderAPIEndpointType) { + this(null, null, traderOAuthToken, traderAPIEndpointType, null, null, null, null, null); } /** - * Instantiates a new {@link AlpacaAPI}. + * Instantiates a new {@link AlpacaAPI}. Use this constructor if you are using the Trading API with OAuth to act on + * behalf of an Alpaca account and a custom {@link OkHttpClient} instance. * - * @param keyID the key ID - * @param secretKey the secret key - * @param endpointAPIType the {@link EndpointAPIType} - * @param dataAPIType the {@link DataAPIType} + * @param traderOAuthToken the Trader OAuth token + * @param traderAPIEndpointType the {@link TraderAPIEndpointType} + * @param okHttpClient an existing {@link OkHttpClient} or null to create a new default + * instance */ - public AlpacaAPI(String keyID, String secretKey, EndpointAPIType endpointAPIType, DataAPIType dataAPIType) { - this(null, keyID, secretKey, null, endpointAPIType, dataAPIType); + public AlpacaAPI(String traderOAuthToken, TraderAPIEndpointType traderAPIEndpointType, OkHttpClient okHttpClient) { + this(null, null, traderOAuthToken, traderAPIEndpointType, null, null, null, null, okHttpClient); } /** - * Instantiates a new {@link AlpacaAPI}. + * Instantiates a new {@link AlpacaAPI}. Use this constructor if you are using the Broker API. + * + * @param brokerAPIKey the Broker API key + * @param brokerAPISecret the Broker API secret + * @param brokerAPIEndpointType the {@link BrokerAPIEndpointType} + */ + public AlpacaAPI(String brokerAPIKey, String brokerAPISecret, BrokerAPIEndpointType brokerAPIEndpointType) { + this(null, null, null, null, null, brokerAPIKey, brokerAPISecret, brokerAPIEndpointType, null); + } + + /** + * Instantiates a new {@link AlpacaAPI}. Use this constructor if you are using the Broker API and a custom + * {@link OkHttpClient} instance. * - * @param oAuthToken the OAuth token. Note that the Data API v2 does not work with OAuth tokens. + * @param brokerAPIKey the Broker API key + * @param brokerAPISecret the Broker API secret + * @param brokerAPIEndpointType the {@link BrokerAPIEndpointType} + * @param okHttpClient an existing {@link OkHttpClient} or null to create a new default + * instance */ - public AlpacaAPI(String oAuthToken) { - this(null, null, null, oAuthToken, - AlpacaProperties.ENDPOINT_API_TYPE, - AlpacaProperties.DATA_API_TYPE); + public AlpacaAPI(String brokerAPIKey, String brokerAPISecret, BrokerAPIEndpointType brokerAPIEndpointType, + OkHttpClient okHttpClient) { + this(null, null, null, null, null, brokerAPIKey, brokerAPISecret, brokerAPIEndpointType, okHttpClient); } /** * Instantiates a new {@link AlpacaAPI}. * - * @param okHttpClient the {@link OkHttpClient} or null to create a default instance - * @param keyID the key ID - * @param secretKey the secret key - * @param oAuthToken the OAuth token - * @param endpointAPIType the {@link EndpointAPIType} - * @param dataAPIType the {@link DataAPIType} + * @param traderKeyID the Trader key ID + * @param traderSecretKey the Trader secret key + * @param traderOAuthToken the Trader OAuth token + * @param traderAPIEndpointType the {@link TraderAPIEndpointType} + * @param marketDataAPISourceType the {@link MarketDataAPISourceType} + * @param brokerAPIKey the Broker API key + * @param brokerAPISecret the Broker API secret + * @param brokerAPIEndpointType the {@link BrokerAPIEndpointType} + * @param okHttpClient an existing {@link OkHttpClient} or null to create a new default + * instance */ - public AlpacaAPI(OkHttpClient okHttpClient, String keyID, String secretKey, String oAuthToken, - EndpointAPIType endpointAPIType, DataAPIType dataAPIType) { - checkArgument((keyID != null && secretKey != null) ^ oAuthToken != null, - "You must specify a (KeyID (%s) and Secret Key (%s)) or an OAuthToken (%s)!", - keyID, secretKey, oAuthToken); - checkNotNull(endpointAPIType); - checkNotNull(dataAPIType); - - // Create default 'okHttpClient' + public AlpacaAPI(String traderKeyID, String traderSecretKey, + String traderOAuthToken, TraderAPIEndpointType traderAPIEndpointType, + MarketDataAPISourceType marketDataAPISourceType, + String brokerAPIKey, String brokerAPISecret, BrokerAPIEndpointType brokerAPIEndpointType, + OkHttpClient okHttpClient) { + this.traderKeyID = traderKeyID; + this.traderSecretKey = traderSecretKey; + this.traderOAuthToken = traderOAuthToken; + this.traderAPIEndpointType = traderAPIEndpointType != null ? traderAPIEndpointType : PAPER; + this.marketDataAPISourceType = marketDataAPISourceType != null ? marketDataAPISourceType : IEX; + this.brokerAPIKey = brokerAPIKey; + this.brokerAPISecret = brokerAPISecret; + this.brokerAPIEndpointType = brokerAPIEndpointType != null ? brokerAPIEndpointType : SANDBOX; + + // Create default OkHttpClient instance if (okHttpClient == null) { - OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder() - .cache(null); // Ensure response caching is disabled + OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); if (LOGGER.isDebugEnabled()) { clientBuilder.addInterceptor(new HttpLoggingInterceptor(LOGGER::debug)); } okHttpClient = clientBuilder.build(); } this.okHttpClient = okHttpClient; - - streamingWebsocket = new StreamingWebsocket(okHttpClient, endpointAPIType, keyID, secretKey, oAuthToken); - cryptoMarketDataWebsocket = new CryptoMarketDataWebsocket(okHttpClient, keyID, secretKey); - stockMarketDataWebsocket = new StockMarketDataWebsocket(okHttpClient, dataAPIType, keyID, secretKey); - newsMarketDataWebsocket = new NewsMarketDataWebsocket(okHttpClient, keyID, secretKey); } /** - * @return the {@link StreamingWebsocketInterface} + * Gets the {@link OkHttpClient}. + * + * @return the {@link OkHttpClient} */ - public StreamingWebsocketInterface streaming() { - return streamingWebsocket; + public OkHttpClient getOkHttpClient() { + return okHttpClient; } /** - * @return the Crypto {@link MarketDataWebsocketInterface} + * Gets the {@link AlpacaTraderAPI}. Lazily instantiated. + * + * @return the {@link AlpacaTraderAPI} */ - public MarketDataWebsocketInterface cryptoMarketDataStreaming() { - return cryptoMarketDataWebsocket; + public synchronized AlpacaTraderAPI trader() { + if (trader == null) { + trader = new AlpacaTraderAPI(traderKeyID, traderSecretKey, traderOAuthToken, traderAPIEndpointType, + okHttpClient); + } + return trader; } /** - * @return the Stock {@link MarketDataWebsocketInterface} + * Gets the {@link AlpacaMarketDataAPI}. Lazily instantiated. + * + * @return the {@link AlpacaMarketDataAPI} */ - public MarketDataWebsocketInterface stockMarketDataStreaming() { - return stockMarketDataWebsocket; + public synchronized AlpacaMarketDataAPI marketData() { + if (marketData == null) { + marketData = new AlpacaMarketDataAPI(traderKeyID, traderSecretKey, brokerAPIKey, brokerAPISecret, + okHttpClient); + } + return marketData; } /** - * @return the News {@link MarketDataWebsocketInterface} + * Gets the {@link AlpacaBrokerAPI}. Lazily instantiated. + * + * @return the {@link AlpacaBrokerAPI} */ - public MarketDataWebsocketInterface newsMarketDataStreaming() { - return newsMarketDataWebsocket; - } - - public OkHttpClient getOkHttpClient() { - return okHttpClient; + public synchronized AlpacaBrokerAPI broker() { + if (broker == null) { + broker = new AlpacaBrokerAPI(brokerAPIKey, brokerAPISecret, brokerAPIEndpointType, okHttpClient); + } + return broker; } /** @@ -171,40 +222,66 @@ public static Builder builder() { */ public static final class Builder { - private String keyID; - private String secretKey; - private EndpointAPIType endpointAPIType; - private DataAPIType dataAPIType; + private String traderKeyID; + private String traderSecretKey; + private String traderOAuthToken; + private TraderAPIEndpointType traderAPIEndpointType; + private MarketDataAPISourceType marketDataAPISourceType; + private String brokerAPIKey; + private String brokerAPISecret; + private BrokerAPIEndpointType brokerAPIEndpointType; + private OkHttpClient okHttpClient; + + private Builder() {} + + public Builder withTraderKeyID(String traderKeyID) { + this.traderKeyID = traderKeyID; + return this; + } + + public Builder withTraderSecretKey(String traderSecretKey) { + this.traderSecretKey = traderSecretKey; + return this; + } + + public Builder withTraderOAuthToken(String traderOAuthToken) { + this.traderOAuthToken = traderOAuthToken; + return this; + } + + public Builder withTraderAPIEndpointType(TraderAPIEndpointType traderAPIEndpointType) { + this.traderAPIEndpointType = traderAPIEndpointType; + return this; + } - private Builder() { - this.keyID = AlpacaProperties.KEY_ID; - this.secretKey = AlpacaProperties.SECRET_KEY; - this.endpointAPIType = AlpacaProperties.ENDPOINT_API_TYPE; - this.dataAPIType = AlpacaProperties.DATA_API_TYPE; + public Builder withMarketDataAPISourceType(MarketDataAPISourceType marketDataAPISourceType) { + this.marketDataAPISourceType = marketDataAPISourceType; + return this; } - public Builder withKeyID(String keyID) { - this.keyID = keyID; + public Builder withBrokerAPIKey(String brokerAPIKey) { + this.brokerAPIKey = brokerAPIKey; return this; } - public Builder withSecretKey(String secretKey) { - this.secretKey = secretKey; + public Builder withBrokerAPISecret(String brokerAPISecret) { + this.brokerAPISecret = brokerAPISecret; return this; } - public Builder withEndpointAPIType(EndpointAPIType endpointAPIType) { - this.endpointAPIType = endpointAPIType; + public Builder withBrokerAPIEndpointType(BrokerAPIEndpointType brokerAPIEndpointType) { + this.brokerAPIEndpointType = brokerAPIEndpointType; return this; } - public Builder withDataAPIType(DataAPIType dataAPIType) { - this.dataAPIType = dataAPIType; + public Builder withOkHttpClient(OkHttpClient okHttpClient) { + this.okHttpClient = okHttpClient; return this; } public AlpacaAPI build() { - return new AlpacaAPI(this); + return new AlpacaAPI(traderKeyID, traderSecretKey, traderOAuthToken, traderAPIEndpointType, + marketDataAPISourceType, brokerAPIKey, brokerAPISecret, brokerAPIEndpointType, okHttpClient); } } } diff --git a/src/main/java/net/jacobpeterson/alpaca/AlpacaBrokerAPI.java b/src/main/java/net/jacobpeterson/alpaca/AlpacaBrokerAPI.java new file mode 100644 index 00000000..7d9851b5 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/AlpacaBrokerAPI.java @@ -0,0 +1,304 @@ +package net.jacobpeterson.alpaca; + +import net.jacobpeterson.alpaca.model.util.apitype.BrokerAPIEndpointType; +import net.jacobpeterson.alpaca.openapi.broker.ApiClient; +import net.jacobpeterson.alpaca.openapi.broker.api.AccountsApi; +import net.jacobpeterson.alpaca.openapi.broker.api.AssetsApi; +import net.jacobpeterson.alpaca.openapi.broker.api.CalendarApi; +import net.jacobpeterson.alpaca.openapi.broker.api.ClockApi; +import net.jacobpeterson.alpaca.openapi.broker.api.CorporateActionsApi; +import net.jacobpeterson.alpaca.openapi.broker.api.CountryInfoApi; +import net.jacobpeterson.alpaca.openapi.broker.api.DocumentsApi; +import net.jacobpeterson.alpaca.openapi.broker.api.EventsApi; +import net.jacobpeterson.alpaca.openapi.broker.api.FundingApi; +import net.jacobpeterson.alpaca.openapi.broker.api.FundingWalletsApi; +import net.jacobpeterson.alpaca.openapi.broker.api.JournalsApi; +import net.jacobpeterson.alpaca.openapi.broker.api.KycApi; +import net.jacobpeterson.alpaca.openapi.broker.api.LogosApi; +import net.jacobpeterson.alpaca.openapi.broker.api.OAuthApi; +import net.jacobpeterson.alpaca.openapi.broker.api.RebalancingApi; +import net.jacobpeterson.alpaca.openapi.broker.api.ReportingApi; +import net.jacobpeterson.alpaca.openapi.broker.api.TradingApi; +import net.jacobpeterson.alpaca.openapi.broker.api.WatchlistApi; +import okhttp3.OkHttpClient; + +import static com.google.common.base.Preconditions.checkNotNull; +import static net.jacobpeterson.alpaca.util.apikey.APIKeyUtil.createBrokerAPIAuthKey; + +/** + * {@link AlpacaBrokerAPI} is the class used to interface with the Alpaca Broker API endpoints. This class is + * thread-safe. + */ +public class AlpacaBrokerAPI { + + private final ApiClient apiClient; + private AccountsApi accounts; + private AssetsApi assets; + private CalendarApi calendar; + private ClockApi clock; + private CorporateActionsApi corporateActions; + private CountryInfoApi countryInfo; + private DocumentsApi documents; + private EventsApi events; + private FundingApi funding; + private FundingWalletsApi fundingWallets; + private JournalsApi journals; + private KycApi kyc; + private LogosApi logos; + private OAuthApi oAuth; + private RebalancingApi rebalancing; + private ReportingApi reporting; + private TradingApi trading; + private WatchlistApi watchlist; + + /** + * Instantiates a new {@link AlpacaBrokerAPI}. + * + * @param brokerAPIKey the Broker API key + * @param brokerAPISecret the Broker API secret + * @param brokerAPIEndpointType the {@link BrokerAPIEndpointType} + * @param okHttpClient an existing {@link OkHttpClient} or null to create a new default + * instance + */ + @SuppressWarnings("UnnecessaryDefault") + AlpacaBrokerAPI(String brokerAPIKey, String brokerAPISecret, BrokerAPIEndpointType brokerAPIEndpointType, + OkHttpClient okHttpClient) { + checkNotNull(brokerAPIKey); + checkNotNull(brokerAPISecret); + checkNotNull(brokerAPIEndpointType); + checkNotNull(okHttpClient); + + apiClient = new ApiClient(okHttpClient); + apiClient.setServerIndex(switch (brokerAPIEndpointType) { + case SANDBOX -> 0; + case PRODUCTION -> 1; + default -> throw new UnsupportedOperationException(); + }); + apiClient.addDefaultHeader("Authorization", createBrokerAPIAuthKey(brokerAPIKey, brokerAPISecret)); + } + + /** + * Gets the internal {@link ApiClient}. + * + * @return the {@link ApiClient} + */ + public ApiClient getInternalAPIClient() { + return apiClient; + } + + /** + * Gets the {@link AccountsApi}. Lazily instantiated. + * + * @return the {@link AccountsApi} + */ + public synchronized AccountsApi accounts() { + if (accounts == null) { + accounts = new AccountsApi(apiClient); + } + return accounts; + } + + /** + * Gets the {@link AssetsApi}. Lazily instantiated. + * + * @return the {@link AssetsApi} + */ + public synchronized AssetsApi assets() { + if (assets == null) { + assets = new AssetsApi(apiClient); + } + return assets; + } + + /** + * Gets the {@link CalendarApi}. Lazily instantiated. + * + * @return the {@link CalendarApi} + */ + public synchronized CalendarApi calendar() { + if (calendar == null) { + calendar = new CalendarApi(apiClient); + } + return calendar; + } + + /** + * Gets the {@link ClockApi}. Lazily instantiated. + * + * @return the {@link ClockApi} + */ + public synchronized ClockApi clock() { + if (clock == null) { + clock = new ClockApi(apiClient); + } + return clock; + } + + /** + * Gets the {@link CorporateActionsApi}. Lazily instantiated. + * + * @return the {@link CorporateActionsApi} + */ + public synchronized CorporateActionsApi corporateActions() { + if (corporateActions == null) { + corporateActions = new CorporateActionsApi(apiClient); + } + return corporateActions; + } + + /** + * Gets the {@link CountryInfoApi}. Lazily instantiated. + * + * @return the {@link CountryInfoApi} + */ + public synchronized CountryInfoApi countryInfo() { + if (countryInfo == null) { + countryInfo = new CountryInfoApi(apiClient); + } + return countryInfo; + } + + /** + * Gets the {@link DocumentsApi}. Lazily instantiated. + * + * @return the {@link DocumentsApi} + */ + public synchronized DocumentsApi documents() { + if (documents == null) { + documents = new DocumentsApi(apiClient); + } + return documents; + } + + /** + * Gets the {@link EventsApi}. Lazily instantiated. + * + * @return the {@link EventsApi} + */ + public synchronized EventsApi events() { + if (events == null) { + events = new EventsApi(apiClient); + } + return events; + } + + /** + * Gets the {@link FundingApi}. Lazily instantiated. + * + * @return the {@link FundingApi} + */ + public synchronized FundingApi funding() { + if (funding == null) { + funding = new FundingApi(apiClient); + } + return funding; + } + + /** + * Gets the {@link FundingWalletsApi}. Lazily instantiated. + * + * @return the {@link FundingWalletsApi} + */ + public synchronized FundingWalletsApi fundingWallets() { + if (fundingWallets == null) { + fundingWallets = new FundingWalletsApi(apiClient); + } + return fundingWallets; + } + + /** + * Gets the {@link JournalsApi}. Lazily instantiated. + * + * @return the {@link JournalsApi} + */ + public synchronized JournalsApi journals() { + if (journals == null) { + journals = new JournalsApi(apiClient); + } + return journals; + } + + /** + * Gets the {@link KycApi}. Lazily instantiated. + * + * @return the {@link KycApi} + */ + public synchronized KycApi kyc() { + if (kyc == null) { + kyc = new KycApi(apiClient); + } + return kyc; + } + + /** + * Gets the {@link LogosApi}. Lazily instantiated. + * + * @return the {@link LogosApi} + */ + public synchronized LogosApi logos() { + if (logos == null) { + logos = new LogosApi(apiClient); + } + return logos; + } + + /** + * Gets the {@link OAuthApi}. Lazily instantiated. + * + * @return the {@link OAuthApi} + */ + public synchronized OAuthApi oAuth() { + if (oAuth == null) { + oAuth = new OAuthApi(apiClient); + } + return oAuth; + } + + /** + * Gets the {@link RebalancingApi}. Lazily instantiated. + * + * @return the {@link RebalancingApi} + */ + public synchronized RebalancingApi rebalancing() { + if (rebalancing == null) { + rebalancing = new RebalancingApi(apiClient); + } + return rebalancing; + } + + /** + * Gets the {@link ReportingApi}. Lazily instantiated. + * + * @return the {@link ReportingApi} + */ + public synchronized ReportingApi reporting() { + if (reporting == null) { + reporting = new ReportingApi(apiClient); + } + return reporting; + } + + /** + * Gets the {@link TradingApi}. Lazily instantiated. + * + * @return the {@link TradingApi} + */ + public synchronized TradingApi trading() { + if (trading == null) { + trading = new TradingApi(apiClient); + } + return trading; + } + + /** + * Gets the {@link WatchlistApi}. Lazily instantiated. + * + * @return the {@link WatchlistApi} + */ + public synchronized WatchlistApi watchlist() { + if (watchlist == null) { + watchlist = new WatchlistApi(apiClient); + } + return watchlist; + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/AlpacaMarketDataAPI.java b/src/main/java/net/jacobpeterson/alpaca/AlpacaMarketDataAPI.java new file mode 100644 index 00000000..1538581a --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/AlpacaMarketDataAPI.java @@ -0,0 +1,152 @@ +package net.jacobpeterson.alpaca; + +import net.jacobpeterson.alpaca.openapi.marketdata.ApiClient; +import net.jacobpeterson.alpaca.openapi.marketdata.api.CorporateActionsApi; +import net.jacobpeterson.alpaca.openapi.marketdata.api.CryptoApi; +import net.jacobpeterson.alpaca.openapi.marketdata.api.ForexApi; +import net.jacobpeterson.alpaca.openapi.marketdata.api.LogosApi; +import net.jacobpeterson.alpaca.openapi.marketdata.api.NewsApi; +import net.jacobpeterson.alpaca.openapi.marketdata.api.OptionApi; +import net.jacobpeterson.alpaca.openapi.marketdata.api.StockApi; +import okhttp3.OkHttpClient; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static net.jacobpeterson.alpaca.util.apikey.APIKeyUtil.createBrokerAPIAuthKey; + +/** + * {@link AlpacaMarketDataAPI} is the class used to interface with the Alpaca Market Data API endpoints. This class is + * thread-safe. + */ +public class AlpacaMarketDataAPI { + + private final ApiClient apiClient; + private CorporateActionsApi corporateActions; + private CryptoApi crypto; + private ForexApi forex; + private LogosApi logos; + private NewsApi news; + private OptionApi option; + private StockApi stock; + + /** + * Instantiates a new {@link AlpacaMarketDataAPI}. + * + * @param traderKeyID the Trader key ID + * @param traderSecretKey the Trader secret key + * @param brokerAPIKey the Broker API key + * @param brokerAPISecret the Broker API secret + * @param okHttpClient an existing {@link OkHttpClient} or null to create a new default instance + */ + AlpacaMarketDataAPI(String traderKeyID, String traderSecretKey, + String brokerAPIKey, String brokerAPISecret, OkHttpClient okHttpClient) { + checkArgument((traderKeyID != null && traderSecretKey != null) ^ + (brokerAPIKey != null && brokerAPISecret != null), + "You must specify a (trader key ID and secret key) or an (broker API key and secret)!"); + checkNotNull(okHttpClient); + + final boolean traderKeysGiven = traderKeyID != null && traderSecretKey != null; + + apiClient = new ApiClient(okHttpClient); + apiClient.setServerIndex(traderKeysGiven ? 0 : 1); + if (traderKeysGiven) { + apiClient.addDefaultHeader("APCA-API-KEY-ID", traderKeyID); + apiClient.addDefaultHeader("APCA-API-SECRET-KEY", traderSecretKey); + } else { + apiClient.addDefaultHeader("Authorization", createBrokerAPIAuthKey(brokerAPIKey, brokerAPISecret)); + } + } + + /** + * Gets the internal {@link ApiClient}. + * + * @return the {@link ApiClient} + */ + public ApiClient getInternalAPIClient() { + return apiClient; + } + + /** + * Gets the {@link CorporateActionsApi}. Lazily instantiated. + * + * @return the {@link CorporateActionsApi} + */ + public synchronized CorporateActionsApi corporateActions() { + if (corporateActions == null) { + corporateActions = new CorporateActionsApi(apiClient); + } + return corporateActions; + } + + /** + * Gets the {@link CryptoApi}. Lazily instantiated. + * + * @return the {@link CryptoApi} + */ + public synchronized CryptoApi crypto() { + if (crypto == null) { + crypto = new CryptoApi(apiClient); + } + return crypto; + } + + /** + * Gets the {@link ForexApi}. Lazily instantiated. + * + * @return the {@link ForexApi} + */ + public synchronized ForexApi forex() { + if (forex == null) { + forex = new ForexApi(apiClient); + } + return forex; + } + + /** + * Gets the {@link LogosApi}. Lazily instantiated. + * + * @return the {@link LogosApi} + */ + public synchronized LogosApi logos() { + if (logos == null) { + logos = new LogosApi(apiClient); + } + return logos; + } + + /** + * Gets the {@link NewsApi}. Lazily instantiated. + * + * @return the {@link NewsApi} + */ + public synchronized NewsApi news() { + if (news == null) { + news = new NewsApi(apiClient); + } + return news; + } + + /** + * Gets the {@link OptionApi}. Lazily instantiated. + * + * @return the {@link OptionApi} + */ + public synchronized OptionApi option() { + if (option == null) { + option = new OptionApi(apiClient); + } + return option; + } + + /** + * Gets the {@link StockApi}. Lazily instantiated. + * + * @return the {@link StockApi} + */ + public synchronized StockApi stock() { + if (stock == null) { + stock = new StockApi(apiClient); + } + return stock; + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/AlpacaTraderAPI.java b/src/main/java/net/jacobpeterson/alpaca/AlpacaTraderAPI.java new file mode 100644 index 00000000..07485e9c --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/AlpacaTraderAPI.java @@ -0,0 +1,212 @@ +package net.jacobpeterson.alpaca; + +import net.jacobpeterson.alpaca.model.util.apitype.TraderAPIEndpointType; +import net.jacobpeterson.alpaca.openapi.trader.ApiClient; +import net.jacobpeterson.alpaca.openapi.trader.api.AccountActivitiesApi; +import net.jacobpeterson.alpaca.openapi.trader.api.AccountConfigurationsApi; +import net.jacobpeterson.alpaca.openapi.trader.api.AccountsApi; +import net.jacobpeterson.alpaca.openapi.trader.api.AssetsApi; +import net.jacobpeterson.alpaca.openapi.trader.api.CalendarApi; +import net.jacobpeterson.alpaca.openapi.trader.api.ClockApi; +import net.jacobpeterson.alpaca.openapi.trader.api.CorporateActionsApi; +import net.jacobpeterson.alpaca.openapi.trader.api.OrdersApi; +import net.jacobpeterson.alpaca.openapi.trader.api.PortfolioHistoryApi; +import net.jacobpeterson.alpaca.openapi.trader.api.PositionsApi; +import net.jacobpeterson.alpaca.openapi.trader.api.WatchlistsApi; +import okhttp3.OkHttpClient; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * {@link AlpacaTraderAPI} is the class used to interface with the Alpaca Trader API endpoints. This class is + * thread-safe. + */ +public class AlpacaTraderAPI { + + private final ApiClient apiClient; + private AccountActivitiesApi accountActivities; + private AccountConfigurationsApi accountConfigurations; + private AccountsApi accounts; + private AssetsApi assets; + private CalendarApi calendar; + private ClockApi clock; + private CorporateActionsApi traderCorporateActions; + private OrdersApi orders; + private PortfolioHistoryApi portfolioHistory; + private PositionsApi positions; + private WatchlistsApi watchlists; + + /** + * Instantiates a new {@link AlpacaTraderAPI}. + * + * @param traderKeyID the Trader key ID + * @param traderSecretKey the Trader secret key + * @param traderOAuthToken the Trader OAuth token + * @param traderAPIEndpointType the {@link TraderAPIEndpointType} + * @param okHttpClient an existing {@link OkHttpClient} or null to create a new default + * instance + */ + @SuppressWarnings("UnnecessaryDefault") + AlpacaTraderAPI(String traderKeyID, String traderSecretKey, String traderOAuthToken, + TraderAPIEndpointType traderAPIEndpointType, OkHttpClient okHttpClient) { + checkArgument((traderKeyID != null && traderSecretKey != null) ^ (traderOAuthToken != null), + "You must specify a (trader key ID and secret key) or an (OAuth token)!"); + checkNotNull(traderAPIEndpointType); + checkNotNull(okHttpClient); + + apiClient = new ApiClient(okHttpClient); + apiClient.setServerIndex(switch (traderAPIEndpointType) { + case PAPER -> 0; + case LIVE -> 1; + default -> throw new UnsupportedOperationException(); + }); + if (traderKeyID != null && traderSecretKey != null) { + apiClient.addDefaultHeader("APCA-API-KEY-ID", traderKeyID); + apiClient.addDefaultHeader("APCA-API-SECRET-KEY", traderSecretKey); + } else { + apiClient.addDefaultHeader("Authorization", "Bearer " + traderOAuthToken); + } + } + + /** + * Gets the internal {@link ApiClient}. + * + * @return the {@link ApiClient} + */ + public ApiClient getInternalAPIClient() { + return apiClient; + } + + /** + * Gets the {@link AccountActivitiesApi}. Lazily instantiated. + * + * @return the {@link AccountActivitiesApi} + */ + public synchronized AccountActivitiesApi accountActivities() { + if (accountActivities == null) { + accountActivities = new AccountActivitiesApi(apiClient); + } + return accountActivities; + } + + /** + * Gets the {@link AccountConfigurationsApi}. Lazily instantiated. + * + * @return the {@link AccountConfigurationsApi} + */ + public synchronized AccountConfigurationsApi accountConfigurations() { + if (accountConfigurations == null) { + accountConfigurations = new AccountConfigurationsApi(apiClient); + } + return accountConfigurations; + } + + /** + * Gets the {@link AccountsApi}. Lazily instantiated. + * + * @return {@link AccountsApi} + */ + public synchronized AccountsApi accounts() { + if (accounts == null) { + accounts = new AccountsApi(apiClient); + } + return accounts; + } + + /** + * Gets the {@link AssetsApi}. Lazily instantiated. + * + * @return {@link AssetsApi} + */ + public synchronized AssetsApi assets() { + if (assets == null) { + assets = new AssetsApi(apiClient); + } + return assets; + } + + /** + * Gets the {@link CalendarApi}. Lazily instantiated. + * + * @return {@link CalendarApi} + */ + public synchronized CalendarApi calendar() { + if (calendar == null) { + calendar = new CalendarApi(apiClient); + } + return calendar; + } + + /** + * Gets the {@link ClockApi}. Lazily instantiated. + * + * @return {@link ClockApi} + */ + public synchronized ClockApi clock() { + if (clock == null) { + clock = new ClockApi(apiClient); + } + return clock; + } + + /** + * Gets the {@link CorporateActionsApi}. Lazily instantiated. + * + * @return the {@link CorporateActionsApi} + */ + public synchronized CorporateActionsApi traderCorporateActions() { + if (traderCorporateActions == null) { + traderCorporateActions = new CorporateActionsApi(apiClient); + } + return traderCorporateActions; + } + + /** + * Gets the {@link OrdersApi}. Lazily instantiated. + * + * @return {@link OrdersApi} + */ + public synchronized OrdersApi orders() { + if (orders == null) { + orders = new OrdersApi(apiClient); + } + return orders; + } + + /** + * Gets the {@link PortfolioHistoryApi}. Lazily instantiated. + * + * @return {@link PortfolioHistoryApi} + */ + public synchronized PortfolioHistoryApi portfolioHistory() { + if (portfolioHistory == null) { + portfolioHistory = new PortfolioHistoryApi(apiClient); + } + return portfolioHistory; + } + + /** + * Gets the {@link PositionsApi}. Lazily instantiated. + * + * @return {@link PositionsApi} + */ + public synchronized PositionsApi positions() { + if (positions == null) { + positions = new PositionsApi(apiClient); + } + return positions; + } + + /** + * Gets the {@link WatchlistsApi}. Lazily instantiated. + * + * @return {@link WatchlistsApi} + */ + public synchronized WatchlistsApi watchlists() { + if (watchlists == null) { + watchlists = new WatchlistsApi(apiClient); + } + return watchlists; + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/properties/AlpacaProperties.java b/src/main/java/net/jacobpeterson/alpaca/properties/AlpacaProperties.java deleted file mode 100644 index 175a83c8..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/properties/AlpacaProperties.java +++ /dev/null @@ -1,29 +0,0 @@ -package net.jacobpeterson.alpaca.properties; - -import net.jacobpeterson.alpaca.AlpacaAPI; -import net.jacobpeterson.alpaca.model.properties.DataAPIType; -import net.jacobpeterson.alpaca.model.properties.EndpointAPIType; - -import static net.jacobpeterson.alpaca.util.properties.PropertyUtil.getProperty; - -/** - * {@link AlpacaProperties} defines properties for {@link AlpacaAPI}. - */ -public class AlpacaProperties { - - private static final String ALPACA_PROPERTIES_FILE = "alpaca.properties"; - private static final String ALPACA_DEFAULT_PROPERTIES_FILE = "alpaca.default.properties"; - - public static final String KEY_ID = getProperty( - ALPACA_PROPERTIES_FILE, ALPACA_DEFAULT_PROPERTIES_FILE, - "key_id"); - public static final String SECRET_KEY = getProperty( - ALPACA_PROPERTIES_FILE, ALPACA_DEFAULT_PROPERTIES_FILE, - "secret_key"); - public static final EndpointAPIType ENDPOINT_API_TYPE = EndpointAPIType.fromValue(getProperty( - ALPACA_PROPERTIES_FILE, ALPACA_DEFAULT_PROPERTIES_FILE, - "endpoint_api_type")); - public static final DataAPIType DATA_API_TYPE = DataAPIType.fromValue(getProperty( - ALPACA_PROPERTIES_FILE, ALPACA_DEFAULT_PROPERTIES_FILE, - "data_api_type")); -} diff --git a/src/main/java/net/jacobpeterson/alpaca/properties/data_api_type.json b/src/main/java/net/jacobpeterson/alpaca/properties/data_api_type.json deleted file mode 100644 index 3a782d77..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/properties/data_api_type.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "type": "string", - "javaName": "DataAPIType", - "title": "Defines the data APIs to use with Alpaca.", - "enum": [ - "iex", - "sip" - ] -} diff --git a/src/main/java/net/jacobpeterson/alpaca/properties/endpoint_api_type.json b/src/main/java/net/jacobpeterson/alpaca/properties/endpoint_api_type.json deleted file mode 100644 index c0eacd4c..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/properties/endpoint_api_type.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "type": "string", - "javaName": "EndpointAPIType", - "title": "Defines the API endpoints to use with Alpaca.", - "enum": [ - "live", - "paper" - ] -} diff --git a/src/main/java/net/jacobpeterson/alpaca/util/apikey/APIKeyUtil.java b/src/main/java/net/jacobpeterson/alpaca/util/apikey/APIKeyUtil.java new file mode 100644 index 00000000..29de58ec --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/util/apikey/APIKeyUtil.java @@ -0,0 +1,23 @@ +package net.jacobpeterson.alpaca.util.apikey; + +import java.util.Base64; + +/** + * {@link APIKeyUtil} is a utility class for Alpaca API keys. + */ +public class APIKeyUtil { + + /** + * Creates a broker API auth key. + * + * @param brokerAPIKey the broker API key + * @param brokerAPISecret the broker API secret + * + * @return the key {@link String} + * + * @see Alpaca Docs + */ + public static String createBrokerAPIAuthKey(String brokerAPIKey, String brokerAPISecret) { + return Base64.getEncoder().encodeToString((brokerAPIKey + ":" + brokerAPISecret).getBytes()); + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/util/apitype/broker_api_endpoint_type.json b/src/main/java/net/jacobpeterson/alpaca/util/apitype/broker_api_endpoint_type.json new file mode 100644 index 00000000..c4dd1c66 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/util/apitype/broker_api_endpoint_type.json @@ -0,0 +1,8 @@ +{ + "type": "string", + "javaName": "BrokerAPIEndpointType", + "enum": [ + "sandbox", + "production" + ] +} diff --git a/src/main/java/net/jacobpeterson/alpaca/util/apitype/market_data_api_source_type.json b/src/main/java/net/jacobpeterson/alpaca/util/apitype/market_data_api_source_type.json new file mode 100644 index 00000000..e0f74cdc --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/util/apitype/market_data_api_source_type.json @@ -0,0 +1,8 @@ +{ + "type": "string", + "javaName": "MarketDataAPISourceType", + "enum": [ + "iex", + "sip" + ] +} diff --git a/src/main/java/net/jacobpeterson/alpaca/util/apitype/trader_api_endpoint_type.json b/src/main/java/net/jacobpeterson/alpaca/util/apitype/trader_api_endpoint_type.json new file mode 100644 index 00000000..574d4109 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/util/apitype/trader_api_endpoint_type.json @@ -0,0 +1,8 @@ +{ + "type": "string", + "javaName": "TraderAPIEndpointType", + "enum": [ + "paper", + "live" + ] +} diff --git a/src/main/java/net/jacobpeterson/alpaca/util/gson/GsonUtil.java b/src/main/java/net/jacobpeterson/alpaca/util/gson/GsonUtil.java deleted file mode 100644 index dc86e2c1..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/util/gson/GsonUtil.java +++ /dev/null @@ -1,28 +0,0 @@ -package net.jacobpeterson.alpaca.util.gson; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import net.jacobpeterson.alpaca.util.gson.adapters.LocalDateAdapter; -import net.jacobpeterson.alpaca.util.gson.adapters.LocalTimeAdapter; -import net.jacobpeterson.alpaca.util.gson.adapters.ZonedDateTimeAdapter; - -import java.time.LocalDate; -import java.time.LocalTime; -import java.time.ZonedDateTime; - -/** - * {@link GsonUtil} contains utility methods relating to {@link Gson}. - */ -public class GsonUtil { - - /** - * A {@link Gson} instance with registered date/time adapters. - */ - public static final Gson GSON = new GsonBuilder() - .registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeAdapter()) - .registerTypeAdapter(LocalDate.class, new LocalDateAdapter()) - .registerTypeAdapter(LocalTime.class, new LocalTimeAdapter()) - .enableComplexMapKeySerialization() - .setLenient() - .create(); -} diff --git a/src/main/java/net/jacobpeterson/alpaca/util/gson/adapters/LocalDateAdapter.java b/src/main/java/net/jacobpeterson/alpaca/util/gson/adapters/LocalDateAdapter.java deleted file mode 100644 index a9475c17..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/util/gson/adapters/LocalDateAdapter.java +++ /dev/null @@ -1,31 +0,0 @@ -package net.jacobpeterson.alpaca.util.gson.adapters; - -import com.google.gson.Gson; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonParseException; -import com.google.gson.JsonPrimitive; -import com.google.gson.JsonSerializationContext; -import com.google.gson.JsonSerializer; - -import java.lang.reflect.Type; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; - -/** - * {@link LocalDateAdapter} is a {@link Gson} adapter for {@link LocalDate}s. - */ -public class LocalDateAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(LocalDate src, Type typeOfSrc, JsonSerializationContext context) { - return new JsonPrimitive(src.format(DateTimeFormatter.ISO_LOCAL_DATE)); - } - - @Override - public LocalDate deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - return LocalDate.parse(json.getAsJsonPrimitive().getAsString(), DateTimeFormatter.ISO_LOCAL_DATE); - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/util/gson/adapters/LocalTimeAdapter.java b/src/main/java/net/jacobpeterson/alpaca/util/gson/adapters/LocalTimeAdapter.java deleted file mode 100644 index 9d6bb735..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/util/gson/adapters/LocalTimeAdapter.java +++ /dev/null @@ -1,31 +0,0 @@ -package net.jacobpeterson.alpaca.util.gson.adapters; - -import com.google.gson.Gson; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonParseException; -import com.google.gson.JsonPrimitive; -import com.google.gson.JsonSerializationContext; -import com.google.gson.JsonSerializer; - -import java.lang.reflect.Type; -import java.time.LocalTime; -import java.time.format.DateTimeFormatter; - -/** - * {@link LocalTimeAdapter} is a {@link Gson} adapter for {@link LocalTime}s. - */ -public class LocalTimeAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(LocalTime localTime, Type typeOfSrc, JsonSerializationContext context) { - return new JsonPrimitive(localTime.format(DateTimeFormatter.ISO_LOCAL_TIME)); - } - - @Override - public LocalTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - return LocalTime.parse(json.getAsJsonPrimitive().getAsString(), DateTimeFormatter.ISO_LOCAL_TIME); - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/util/gson/adapters/ZonedDateTimeAdapter.java b/src/main/java/net/jacobpeterson/alpaca/util/gson/adapters/ZonedDateTimeAdapter.java deleted file mode 100644 index e10f1be1..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/util/gson/adapters/ZonedDateTimeAdapter.java +++ /dev/null @@ -1,31 +0,0 @@ -package net.jacobpeterson.alpaca.util.gson.adapters; - -import com.google.gson.Gson; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonParseException; -import com.google.gson.JsonPrimitive; -import com.google.gson.JsonSerializationContext; -import com.google.gson.JsonSerializer; - -import java.lang.reflect.Type; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; - -/** - * {@link ZonedDateTimeAdapter} is a {@link Gson} adapter for {@link ZonedDateTime}s. - */ -public class ZonedDateTimeAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(ZonedDateTime src, Type typeOfSrc, JsonSerializationContext context) { - return new JsonPrimitive(src.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); - } - - @Override - public ZonedDateTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - return ZonedDateTime.parse(json.getAsJsonPrimitive().getAsString(), DateTimeFormatter.ISO_DATE_TIME); - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/util/properties/PropertyUtil.java b/src/main/java/net/jacobpeterson/alpaca/util/properties/PropertyUtil.java deleted file mode 100644 index 718d02a3..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/util/properties/PropertyUtil.java +++ /dev/null @@ -1,146 +0,0 @@ -package net.jacobpeterson.alpaca.util.properties; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Properties; - -/** - * {@link PropertyUtil} is a utility class for all {@link Properties}-related handling. - */ -public final class PropertyUtil { - - private static final Logger LOGGER = LoggerFactory.getLogger(PropertyUtil.class); - - public static final Map CACHED_PROPERTIES = Collections.synchronizedMap(new HashMap<>()); - - /** - * Gets a string property from a property file. Will try to IO load the properties in the property file if not - * cached already. - * - * @param propertyFile the property file - * @param key the key - * - * @return the string - */ - public static String getProperty(String propertyFile, String key) { - return getProperty(propertyFile, null, key, null); - } - - /** - * Gets a string property from a property file. Will try to IO load the properties in the property file if not - * cached already. If the desired property is not found in the propertyFile, then the - * defaultPropertyFile is searched. - * - * @param propertyFile the property file - * @param defaultPropertyFile the default property file - * @param key the key - * - * @return the string - */ - public static String getProperty(String propertyFile, String defaultPropertyFile, String key) { - return getProperty(propertyFile, defaultPropertyFile, key, null); - } - - /** - * Gets a string property from a property file. Will try to IO load the properties in the property file if not - * cached already. If the desired property is not found in the propertyFile, then the - * defaultPropertyFile is searched, and if it's not there, then defaultValue is returned. - * - * @param propertyFile the property file - * @param defaultPropertyFile the default property file - * @param key the key - * @param defaultValue the default value (if the desired property wasn't found, then this is returned) - * - * @return the string - */ - public static String getProperty(String propertyFile, String defaultPropertyFile, String key, String defaultValue) { - final Properties properties; - if (!CACHED_PROPERTIES.containsKey(propertyFile)) { - properties = loadPropertyFile(propertyFile, defaultPropertyFile); - CACHED_PROPERTIES.put(propertyFile, properties); - } else { - properties = CACHED_PROPERTIES.get(propertyFile); - } - return properties == null ? defaultValue : properties.getProperty(key, defaultValue); - } - - /** - * Loads property file {@link Properties}. - * - * @param propertyFile the property file name - * @param defaultPropertyFile the default property file name - * - * @return the properties - */ - public synchronized static Properties loadPropertyFile(String propertyFile, String defaultPropertyFile) { - ClassLoader classLoader = PropertyUtil.class.getClassLoader(); - if (classLoader == null) { - classLoader = ClassLoader.getSystemClassLoader(); - } - - // Load the default property file if exists - Properties defaultProperties = null; - if (defaultPropertyFile != null) { - try (InputStream defaultPropertyStream = classLoader.getResourceAsStream(defaultPropertyFile)) { - if (defaultPropertyStream == null) { - LOGGER.warn("No default property file \"{}\" exists on the classpath.", defaultPropertyFile); - } else { - - defaultProperties = new Properties(); - try { - defaultProperties.load(defaultPropertyStream); - LOGGER.debug("Loaded default properties file: {}", defaultPropertyFile); - } catch (IOException exception) { - LOGGER.error("Could not load default property file: {}", defaultPropertyFile, exception); - } - } - } catch (Exception exception) { - LOGGER.error("Could not read default property file stream: {}", defaultPropertyFile, exception); - } - } else { - LOGGER.warn("No default property file given for: {}", propertyFile); - } - - // Load the property file if exists - Properties properties = null; - if (propertyFile != null) { - try (InputStream propertyStream = classLoader.getResourceAsStream(propertyFile)) { - if (propertyStream == null) { - LOGGER.warn("No property file \"{}\" exists on the classpath.", propertyFile); - } else { - // Add default properties if they were found - properties = defaultProperties == null ? new Properties() : new Properties(defaultProperties); - - // Load the properties - try { - properties.load(propertyStream); - LOGGER.info("Loaded properties file: {}", propertyFile); - } catch (IOException exception) { - LOGGER.error("Could not load property file: {}", propertyFile, exception); - } - } - } catch (Exception exception) { - LOGGER.error("Could not read property file stream: {}", propertyFile, exception); - } - } else if (defaultProperties == null) { - throw new IllegalStateException("No property files were found!"); - } - - if (properties == null) { - LOGGER.debug("Could not find property file: {}", propertyFile); - - if (defaultProperties != null) { - LOGGER.info("Using default properties for: {}", propertyFile); - properties = defaultProperties; - } - } - - return properties; - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java deleted file mode 100644 index 59208ccc..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java +++ /dev/null @@ -1,292 +0,0 @@ -package net.jacobpeterson.alpaca.websocket; - -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.WebSocket; -import okhttp3.WebSocketListener; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ForkJoinPool; -import java.util.concurrent.Future; - -import static com.google.common.base.Preconditions.checkNotNull; - -/** - * {@link AlpacaWebsocket} represents an abstract websocket for Alpaca. - * - * @param the {@link AlpacaWebsocketMessageListener} type parameter - * @param the 'message type' type parameter - * @param the 'message' type parameter - */ -public abstract class AlpacaWebsocket> extends WebSocketListener - implements AlpacaWebsocketInterface { - - /** - * Defines a websocket normal closure code. - * - * @see WebSocket#close(int, String) - */ - public static final int WEBSOCKET_NORMAL_CLOSURE_CODE = 1000; - - /** - * Defines a websocket normal closure message. - * - * @see WebSocket#close(int, String) - */ - public static final String WEBSOCKET_NORMAL_CLOSURE_MESSAGE = "Normal closure"; - - /** - * Defines the maximum number of reconnection attempts to be made by an {@link AlpacaWebsocket}. - */ - public static int MAX_RECONNECT_ATTEMPTS = 5; - - /** - * Defines the millisecond sleep interval between reconnection attempts made by an {@link AlpacaWebsocket}. - */ - public static int RECONNECT_SLEEP_INTERVAL = 1000; - - private static final Logger LOGGER = LoggerFactory.getLogger(AlpacaWebsocket.class); - - protected final OkHttpClient okHttpClient; - protected final HttpUrl websocketURL; - protected final String websocketName; - protected final String keyID; - protected final String secretKey; - protected final String oAuthToken; - protected final boolean useOAuth; - - protected L listener; - protected WebsocketStateListener websocketStateListener; - protected WebSocket websocket; - protected boolean connected; - protected boolean authenticated; - protected CompletableFuture authenticationMessageFuture; - protected boolean intentionalClose; - protected int reconnectAttempts; - protected boolean automaticallyReconnect; - - /** - * Instantiates a {@link AlpacaWebsocket}. - * - * @param okHttpClient the {@link OkHttpClient} - * @param websocketURL the websocket {@link HttpUrl} - * @param websocketName the websocket name - * @param keyID the key ID - * @param secretKey the secret key - * @param oAuthToken the OAuth token - */ - public AlpacaWebsocket(OkHttpClient okHttpClient, HttpUrl websocketURL, String websocketName, - String keyID, String secretKey, String oAuthToken) { - checkNotNull(okHttpClient); - checkNotNull(websocketURL); - checkNotNull(websocketName); - - this.okHttpClient = okHttpClient; - this.websocketURL = websocketURL; - this.websocketName = websocketName; - this.keyID = keyID; - this.secretKey = secretKey; - this.oAuthToken = oAuthToken; - useOAuth = oAuthToken != null; - - automaticallyReconnect = true; - } - - @Override - public void connect() { - if (!isConnected()) { - Request websocketRequest = new Request.Builder() - .url(websocketURL) - .get() - .build(); - websocket = okHttpClient.newWebSocket(websocketRequest, this); - } - } - - @Override - public void disconnect() { - if (websocket != null && isConnected()) { - intentionalClose = true; - websocket.close(WEBSOCKET_NORMAL_CLOSURE_CODE, WEBSOCKET_NORMAL_CLOSURE_MESSAGE); - } else { - cleanupState(); - } - } - - @Override - public boolean isConnected() { - return connected; - } - - @Override - public boolean isAuthenticated() { - return authenticated; - } - - @Override - public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) { - connected = true; - - LOGGER.info("{} websocket opened.", websocketName); - LOGGER.debug("{} websocket response: {}", websocketName, response); - - // Call 'onConnection' or 'onReconnection' async to avoid any potential dead-locking since this is called - // in sync with 'onMessage' in OkHttp's 'WebSocketListener' - ForkJoinPool.commonPool().execute(() -> { - if (reconnectAttempts > 0) { - reconnectAttempts = 0; - onReconnection(); - } else { - onConnection(); - } - }); - - if (websocketStateListener != null) { - websocketStateListener.onOpen(response); - } - } - - @Override - public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) { - connected = false; - - if (intentionalClose) { - LOGGER.info("{} websocket closed.", websocketName); - LOGGER.debug("Close code: {}, Reason: {}", code, reason); - cleanupState(); - } else { - LOGGER.error("{} websocket closed unintentionally! Code: {}, Reason: {}", websocketName, code, reason); - handleReconnectionAttempt(); - } - - if (websocketStateListener != null) { - websocketStateListener.onClosed(code, reason); - } - } - - @Override - public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable cause, @Nullable Response response) { - LOGGER.error("{} websocket failure!", websocketName, cause); - - // A websocket failure occurs when either there is a connection failure or when the client throws - // an Exception when receiving a message. In either case, OkHttp will silently close the websocket - // connection, so try to reopen it. - connected = false; - handleReconnectionAttempt(); - - if (websocketStateListener != null) { - websocketStateListener.onFailure(cause); - } - } - - /** - * Attempts to reconnect the disconnected {@link #websocket} asynchronously. - */ - private void handleReconnectionAttempt() { - if (!automaticallyReconnect) { - return; - } - - if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { - LOGGER.info("Attempting to reconnect {} websocket in {} milliseconds...", - websocketName, RECONNECT_SLEEP_INTERVAL); - - reconnectAttempts++; - - ForkJoinPool.commonPool().execute(() -> { - try { - Thread.sleep(RECONNECT_SLEEP_INTERVAL); - } catch (InterruptedException ignored) { - return; - } - - connect(); - }); - } else { - LOGGER.error("Exhausted {} reconnection attempts. Not attempting to reconnect.", MAX_RECONNECT_ATTEMPTS); - cleanupState(); - } - } - - /** - * Cleans up this instance's state variables. - */ - protected void cleanupState() { - websocket = null; - connected = false; - authenticated = false; - if (authenticationMessageFuture != null && !authenticationMessageFuture.isDone()) { - authenticationMessageFuture.complete(false); - } - authenticationMessageFuture = null; - intentionalClose = false; - reconnectAttempts = 0; - } - - /** - * Called asynchronously when a websocket connection is made. - */ - protected abstract void onConnection(); - - /** - * Called asynchronously when a websocket reconnection is made after unintentional disconnection. - */ - protected abstract void onReconnection(); - - /** - * Sends an authentication message to authenticate this websocket stream. - */ - protected abstract void sendAuthenticationMessage(); - - @Override - public Future getAuthorizationFuture() { - if (authenticationMessageFuture == null) { - authenticationMessageFuture = new CompletableFuture<>(); - } - - return authenticationMessageFuture; - } - - /** - * Calls the {@link AlpacaWebsocketMessageListener}. - * - * @param messageType the message type - * @param message the message - */ - protected void callListener(T messageType, M message) { - if (listener != null) { - try { - listener.onMessage(messageType, message); - } catch (Exception exception) { - LOGGER.error("{} listener threw exception!", websocketName, exception); - } - } - } - - @Override - public void setListener(L listener) { - this.listener = listener; - } - - public WebsocketStateListener getWebsocketStateListener() { - return websocketStateListener; - } - - public void setWebsocketStateListener(WebsocketStateListener websocketStateListener) { - this.websocketStateListener = websocketStateListener; - } - - public boolean doesAutomaticallyReconnect() { - return automaticallyReconnect; - } - - public void setAutomaticallyReconnect(boolean automaticallyReconnect) { - this.automaticallyReconnect = automaticallyReconnect; - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketInterface.java b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketInterface.java deleted file mode 100644 index f524e1df..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketInterface.java +++ /dev/null @@ -1,81 +0,0 @@ -package net.jacobpeterson.alpaca.websocket; - -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -/** - * {@link AlpacaWebsocketInterface} defines an interface for Alpaca websockets. - * - * @param the {@link AlpacaWebsocketMessageListener} type parameter - */ -public interface AlpacaWebsocketInterface> { - - /** - * Connects this Websocket. - */ - void connect(); - - /** - * Disconnects this Websocket. - */ - void disconnect(); - - /** - * Returns true if this websocket is connected. - * - * @return a boolean - */ - boolean isConnected(); - - /** - * Returns true if this websocket is authenticated. - * - * @return a boolean - */ - boolean isAuthenticated(); - - /** - * True if {@link #isConnected()} and {@link #isAuthenticated()}. - * - * @return a boolean - */ - default boolean isValid() { - return isConnected() && isAuthenticated(); - } - - /** - * Gets a {@link Boolean} {@link Future} that completes when an authentication message that is received after a new - * websocket connection indicates successful authentication. - *
- * Note that if this {@link AlpacaWebsocketInterface} is already authorized, the returned {@link Future} will likely - * never complete. - * - * @return a {@link Boolean} {@link Future} - */ - Future getAuthorizationFuture(); - - /** - * Waits for {@link #getAuthorizationFuture()} to complete and returns its value, except when timeout - * time has elapsed, then this will return false. - * - * @param timeout the timeout time - * @param unit the timeout {@link TimeUnit} - * - * @return a boolean - */ - default boolean waitForAuthorization(long timeout, TimeUnit unit) { - try { - return getAuthorizationFuture().get(timeout, unit); - } catch (InterruptedException | ExecutionException | TimeoutException ignored) {} - return false; - } - - /** - * Sets the {@link AlpacaWebsocketMessageListener} for this {@link AlpacaWebsocketInterface}. - * - * @param listener the {@link AlpacaWebsocketMessageListener} - */ - void setListener(L listener); -} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketMessageListener.java b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketMessageListener.java deleted file mode 100644 index da55535c..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketMessageListener.java +++ /dev/null @@ -1,20 +0,0 @@ -package net.jacobpeterson.alpaca.websocket; - -/** - * {@link AlpacaWebsocketMessageListener} defines a listener interface for messages from an {@link AlpacaWebsocket} - * instances. - * - * @param the 'message type' type parameter - * @param the 'message' type parameter - */ -@FunctionalInterface -public interface AlpacaWebsocketMessageListener { - - /** - * Called when a message is received. - * - * @param messageType the message type - * @param message the message - */ - void onMessage(T messageType, M message); -} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/WebsocketStateListener.java b/src/main/java/net/jacobpeterson/alpaca/websocket/WebsocketStateListener.java deleted file mode 100644 index 4f3713b6..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/WebsocketStateListener.java +++ /dev/null @@ -1,32 +0,0 @@ -package net.jacobpeterson.alpaca.websocket; - -import okhttp3.Response; -import okhttp3.WebSocket; - -/** - * {@link WebsocketStateListener} is an interface to listen for various state changes in an {@link WebSocket} instance. - */ -public interface WebsocketStateListener { - - /** - * Called when the {@link WebSocket} is connected. - * - * @param response the HTTP websocket upgrade {@link Response} - */ - void onOpen(Response response); - - /** - * Called when the {@link WebSocket} is disconnected. - * - * @param code the code - * @param reason the reason - */ - void onClosed(int code, String reason); - - /** - * Called when a {@link WebSocket} error has occurred. - * - * @param cause the cause {@link Throwable} - */ - void onFailure(Throwable cause); -} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataListener.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataListener.java deleted file mode 100644 index 0f760d7e..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataListener.java +++ /dev/null @@ -1,11 +0,0 @@ -package net.jacobpeterson.alpaca.websocket.marketdata; - -import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.MarketDataMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.enums.MarketDataMessageType; -import net.jacobpeterson.alpaca.websocket.AlpacaWebsocketMessageListener; - -/** - * {@link MarketDataListener} defines a listener interface for {@link MarketDataWebsocket} messages. - */ -public interface MarketDataListener extends AlpacaWebsocketMessageListener { -} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java deleted file mode 100644 index 48c297f9..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java +++ /dev/null @@ -1,387 +0,0 @@ -package net.jacobpeterson.alpaca.websocket.marketdata; - -import com.google.common.collect.Iterables; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.MarketDataMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.bar.BarMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.control.ErrorMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.control.SubscriptionsMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.control.SuccessMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.enums.MarketDataMessageType; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.quote.QuoteMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.trade.TradeMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.news.common.NewsArticle; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.news.realtime.NewsMessage; -import net.jacobpeterson.alpaca.websocket.AlpacaWebsocket; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.WebSocket; -import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.lang.reflect.Type; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; -import static com.google.common.base.Predicates.not; -import static net.jacobpeterson.alpaca.util.gson.GsonUtil.GSON; - -/** - * {@link MarketDataWebsocket} is an {@link AlpacaWebsocket} implementation and provides the - * {@link MarketDataWebsocketInterface} interface for - * Realtime Market Data for both crypto - * and stocks. - */ -public abstract class MarketDataWebsocket - extends AlpacaWebsocket - implements MarketDataWebsocketInterface { - - private static final Logger LOGGER = LoggerFactory.getLogger(MarketDataWebsocket.class); - private static final String MESSAGE_TYPE_ELEMENT_KEY = "T"; - private static final List AUTH_FAILURE_MESSAGES = Arrays.asList( - "auth failed", - "auth timeout", - "not authenticated"); - private static final List SUBSCRIBABLE_MARKET_DATA_MESSAGE_TYPES = Arrays.asList( - MarketDataMessageType.TRADE, - MarketDataMessageType.QUOTE, - MarketDataMessageType.BAR, - MarketDataMessageType.NEWS); - - /** - * Creates a {@link HttpUrl} for {@link MarketDataWebsocket} with the given websocketURLPathSegments. - * - * @param websocketURLPathSegments the websocket URL path segments - * - * @return a {@link HttpUrl} - */ - private static HttpUrl createWebsocketURL(String websocketURLPathSegments) { - return new HttpUrl.Builder() - .scheme("https") // HttpUrl.Builder doesn't recognize "wss" scheme, but "https" works fine - .host("stream.data.alpaca.markets") - .addPathSegments(websocketURLPathSegments) - .build(); - } - - private final Set listenedMarketDataMessageTypes; - private final Set subscribedTrades; - private final Set subscribedQuotes; - private final Set subscribedBars; - private final Set subscribedNews; - private final Type tradeClassType; - private final Type quoteClassType; - private final Type barClassType; - private final Type newsClassType = NewsMessage.class; - - /** - * Instantiates a new {@link MarketDataWebsocket}. - * - * @param okHttpClient the {@link OkHttpClient} - * @param websocketURLPathSegments the websocket URL path segments - * @param websocketMarketDataTypeName the websocket market data type name {@link String} - * @param keyID the key ID - * @param secretKey the secret key - * @param tradeClass the {@link TradeMessage} {@link Class} to deserialize data into - * @param quoteClass the {@link QuoteMessage} {@link Class} to deserialize data into - * @param barClass the {@link BarMessage} {@link Class} to deserialize data into - */ - public MarketDataWebsocket(OkHttpClient okHttpClient, String websocketURLPathSegments, - String websocketMarketDataTypeName, String keyID, String secretKey, - Class tradeClass, Class quoteClass, - Class barClass) { - super(okHttpClient, createWebsocketURL(websocketURLPathSegments), websocketMarketDataTypeName + " Market Data", - keyID, secretKey, null); - - listenedMarketDataMessageTypes = new HashSet<>(); - subscribedTrades = new HashSet<>(); - subscribedQuotes = new HashSet<>(); - subscribedBars = new HashSet<>(); - subscribedNews = new HashSet<>(); - this.tradeClassType = tradeClass; - this.quoteClassType = quoteClass; - this.barClassType = barClass; - } - - @Override - protected void cleanupState() { - super.cleanupState(); - - listenedMarketDataMessageTypes.clear(); - } - - @Override - protected void onConnection() { - sendAuthenticationMessage(); - } - - @Override - protected void onReconnection() { - sendAuthenticationMessage(); - if (waitForAuthorization(5, TimeUnit.SECONDS)) { - subscribeToControl(Iterables.toArray(listenedMarketDataMessageTypes, MarketDataMessageType.class)); - subscribe(subscribedTrades, subscribedQuotes, subscribedBars, subscribedNews); - } - } - - @Override - protected void sendAuthenticationMessage() { - // Ensures that 'authenticationMessageFuture' exists - getAuthorizationFuture(); - - /* Format of message is: - * { - * "action": "auth", - * "key": "{APCA-API-KEY-ID}", - * "secret": "{APCA-API-SECRET-KEY}" - * } - */ - - JsonObject authObject = new JsonObject(); - authObject.addProperty("action", "auth"); - authObject.addProperty("key", keyID); - authObject.addProperty("secret", secretKey); - - LOGGER.info("{} websocket sending authentication message...", websocketName); - websocket.send(authObject.toString()); - } - - // This websocket uses string frames and not binary frames. - @Override - public void onMessage(@NotNull WebSocket webSocket, @NotNull String message) { - LOGGER.trace("{}", message); - - JsonElement messageElement = JsonParser.parseString(message); - checkState(messageElement instanceof JsonArray, "Message must be a JsonArray! Received: %s", messageElement); - - JsonArray messageArray = messageElement.getAsJsonArray(); - for (JsonElement arrayElement : messageArray) { - checkState(arrayElement instanceof JsonObject, - "'arrayElement' must be a JsonObject! Received: %s", arrayElement); - - JsonObject messageObject = arrayElement.getAsJsonObject(); - MarketDataMessageType marketDataMessageType = GSON.fromJson( - messageObject.get(MESSAGE_TYPE_ELEMENT_KEY), MarketDataMessageType.class); - checkNotNull(marketDataMessageType, "MarketDataMessageType not found in message: %s", messageObject); - - MarketDataMessage marketDataMessage; - switch (marketDataMessageType) { - case SUCCESS: - marketDataMessage = GSON.fromJson(messageObject, SuccessMessage.class); - - if (isSuccessMessageAuthenticated((SuccessMessage) marketDataMessage)) { - LOGGER.info("{} websocket authenticated.", websocketName); - - authenticated = true; - - if (authenticationMessageFuture != null) { - authenticationMessageFuture.complete(true); - } - } - break; - case ERROR: - marketDataMessage = GSON.fromJson(messageObject, ErrorMessage.class); - - if (isErrorMessageAuthFailure((ErrorMessage) marketDataMessage) && - authenticationMessageFuture != null) { - LOGGER.error("{} websocket not authenticated! Received: {}.", websocketName, marketDataMessage); - - authenticated = false; - - if (authenticationMessageFuture != null) { - authenticationMessageFuture.complete(false); - } - } else { - LOGGER.error("{} websocket error message: {}", websocketName, marketDataMessage); - } - break; - case SUBSCRIPTION: - marketDataMessage = GSON.fromJson(messageObject, SubscriptionsMessage.class); - - // Update 'listenedMarketDataMessageTypes' and the associated subscribed symbols lists - SubscriptionsMessage subscriptionsMessage = (SubscriptionsMessage) marketDataMessage; - handleSubscriptionMessageList(MarketDataMessageType.TRADE, subscriptionsMessage.getTrades(), - subscribedTrades); - handleSubscriptionMessageList(MarketDataMessageType.QUOTE, subscriptionsMessage.getQuotes(), - subscribedQuotes); - handleSubscriptionMessageList(MarketDataMessageType.BAR, subscriptionsMessage.getBars(), - subscribedBars); - handleSubscriptionMessageList(MarketDataMessageType.NEWS, subscriptionsMessage.getNews(), - subscribedNews); - break; - case TRADE: - marketDataMessage = GSON.fromJson(messageObject, tradeClassType); - break; - case QUOTE: - marketDataMessage = GSON.fromJson(messageObject, quoteClassType); - break; - case BAR: - marketDataMessage = GSON.fromJson(messageObject, barClassType); - break; - case NEWS: - marketDataMessage = GSON.fromJson(messageObject, newsClassType); - break; - default: - LOGGER.error("Message type {} not implemented!", marketDataMessageType); - continue; - } - - if (listenedMarketDataMessageTypes.contains(marketDataMessageType)) { - callListener(marketDataMessageType, marketDataMessage); - } - } - } - - /** - * Returns true if {@link SuccessMessage#getMessage()} equals "authenticated". - * - * @param successMessage the {@link SuccessMessage} - * - * @return a boolean - */ - private boolean isSuccessMessageAuthenticated(SuccessMessage successMessage) { - return successMessage.getMessage().equalsIgnoreCase("authenticated"); - } - - /** - * Handles a {@link SubscriptionsMessage} for updating {@link #listenedMarketDataMessageTypes} and returns a - * {@link List} of currently subscribe symbols or null. - * - * @param marketDataMessageType the {@link MarketDataMessageType} - * @param newSubscribedSymbols the new subscribed symbols from {@link SubscriptionsMessage} - * @param currentSubscribeSymbols the currently subscribe symbols {@link Set} - */ - private void handleSubscriptionMessageList(MarketDataMessageType marketDataMessageType, - Collection newSubscribedSymbols, Set currentSubscribeSymbols) { - if (newSubscribedSymbols != null && !newSubscribedSymbols.isEmpty()) { - listenedMarketDataMessageTypes.add(marketDataMessageType); - currentSubscribeSymbols.clear(); - currentSubscribeSymbols.addAll(newSubscribedSymbols); - } else { - listenedMarketDataMessageTypes.remove(marketDataMessageType); - currentSubscribeSymbols.clear(); - } - } - - /** - * Returns true if {@link ErrorMessage#getMessage()} is any of {@link #AUTH_FAILURE_MESSAGES}. - * - * @param errorMessage the {@link ErrorMessage} - * - * @return a boolean - */ - private boolean isErrorMessageAuthFailure(ErrorMessage errorMessage) { - return AUTH_FAILURE_MESSAGES.contains(errorMessage.getMessage().toLowerCase()); - } - - @Override - public void subscribeToControl(MarketDataMessageType... marketDataMessageTypes) { - if (marketDataMessageTypes == null) { - return; - } - - Arrays.stream(marketDataMessageTypes) - .filter(not(SUBSCRIBABLE_MARKET_DATA_MESSAGE_TYPES::contains)) - .forEach(listenedMarketDataMessageTypes::add); - } - - @Override - public void subscribe(Collection tradeSymbols, Collection quoteSymbols, - Collection barSymbols, Collection newsSymbols) { - sendSubscriptionUpdate(tradeSymbols, quoteSymbols, barSymbols, newsSymbols, true); - } - - @Override - public void unsubscribe(Collection tradeSymbols, Collection quoteSymbols, - Collection barSymbols, Collection newsSymbols) { - sendSubscriptionUpdate(tradeSymbols, quoteSymbols, barSymbols, newsSymbols, false); - } - - /** - * Sends a subscription update request. - * - * @param tradeSymbols a {@link Collection} of symbols to update for trades or null for no change - * @param quoteSymbols a {@link Collection} of symbols to update for quotes or null for no change - * @param barSymbols a {@link Collection} of symbols to update for bars or null for no change - * @param subscribe true to subscribe, false to unsubscribe - */ - private void sendSubscriptionUpdate(Collection tradeSymbols, Collection quoteSymbols, - Collection barSymbols, Collection newsSymbols, boolean subscribe) { - if (!isConnected()) { - throw new IllegalStateException("This websocket must be connected before sending subscription updates!"); - } - - /* Format of message is: - * { - * "action": "subscribe", - * "trades": ["AAPL"], - * "quotes": ["AMD", "CLDR"], - * "bars": ["*"] - * } - */ - - JsonObject subscriptionUpdateObject = new JsonObject(); - subscriptionUpdateObject.addProperty("action", subscribe ? "subscribe" : "unsubscribe"); - - addSubscriptionUpdateList(subscriptionUpdateObject, "trades", tradeSymbols); - addSubscriptionUpdateList(subscriptionUpdateObject, "quotes", quoteSymbols); - addSubscriptionUpdateList(subscriptionUpdateObject, "bars", barSymbols); - addSubscriptionUpdateList(subscriptionUpdateObject, "news", newsSymbols); - - boolean updateExists = subscriptionUpdateObject.size() > 1; - if (updateExists) { - websocket.send(subscriptionUpdateObject.toString()); - LOGGER.info("Requested subscriptions update: {}.", subscriptionUpdateObject); - } - } - - /** - * Adds symbols to subscriptionUpdateObject with elementKey if not empty. - * - * @param subscriptionUpdateObject the subscription update {@link JsonObject} - * @param elementKey the element key - * @param symbols the symbols {@link Collection} - */ - private void addSubscriptionUpdateList(JsonObject subscriptionUpdateObject, String elementKey, - Collection symbols) { - if (symbols != null && !symbols.isEmpty()) { - JsonArray symbolArray = new JsonArray(); - symbols.forEach(symbolArray::add); - subscriptionUpdateObject.add(elementKey, symbolArray); - } - } - - @Override - public Collection subscribedControls() { - return new HashSet<>(listenedMarketDataMessageTypes); - } - - @Override - public Collection subscribedTrades() { - return new HashSet<>(subscribedTrades); - } - - @Override - public Collection subscribedQuotes() { - return new HashSet<>(subscribedQuotes); - } - - @Override - public Collection subscribedBars() { - return new HashSet<>(subscribedBars); - } - - @Override - public Collection subscribedNews() { - return new HashSet<>(subscribedNews); - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocketInterface.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocketInterface.java deleted file mode 100644 index d4f7700c..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocketInterface.java +++ /dev/null @@ -1,86 +0,0 @@ -package net.jacobpeterson.alpaca.websocket.marketdata; - -import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.MarketDataMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.control.ErrorMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.control.SubscriptionsMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.control.SuccessMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.enums.MarketDataMessageType; -import net.jacobpeterson.alpaca.websocket.AlpacaWebsocketInterface; - -import java.util.Collection; - -/** - * {@link MarketDataWebsocketInterface} is an {@link AlpacaWebsocketInterface} for a {@link MarketDataWebsocket}. - */ -public interface MarketDataWebsocketInterface extends AlpacaWebsocketInterface { - - /** - * Subscribe to a specific control {@link MarketDataMessage} which contain information about the stream's current - * state. That is, an {@link ErrorMessage}, {@link SubscriptionsMessage}, or {@link SuccessMessage}. - * - * @param marketDataMessageTypes array containing any of the following: {@link MarketDataMessageType#SUCCESS}, - * {@link MarketDataMessageType#ERROR}, or {@link MarketDataMessageType#SUBSCRIPTION} - */ - void subscribeToControl(MarketDataMessageType... marketDataMessageTypes); - - /** - * Subscribes to trades, quotes, or bars according to the given {@link Collection} of symbols. - *
- * Note that any one of the given {@link Collection}s can contain the wildcard character e.g. "*" to subscribe to - * ALL available symbols. - * - * @param tradeSymbols a {@link Collection} of symbols to subscribe to trades or null for no change - * @param quoteSymbols a {@link Collection} of symbols to subscribe to quotes or null for no change - * @param barSymbols a {@link Collection} of symbols to subscribe to bars or null for no change - * - * @see #unsubscribe(Collection, Collection, Collection, Collection) - */ - void subscribe(Collection tradeSymbols, Collection quoteSymbols, Collection barSymbols, Collection newsSymbols); - - /** - * Unsubscribes from trades, quotes, or bars according to the given {@link Collection} of symbols. - *
- * Note that any one of the given {@link Collection}s can contain the wildcard character (e.g. "*") to unsubscribe - * from a previously subscribed wildcard. - * - * @param tradeSymbols a {@link Collection} of symbols to unsubscribe from trades or null for no - * change - * @param quoteSymbols a {@link Collection} of symbols to unsubscribe from quotes or null for no - * change - * @param barSymbols a {@link Collection} of symbols to unsubscribe from bars or null for no change - * - * @see #subscribe(Collection, Collection, Collection, Collection) - */ - void unsubscribe(Collection tradeSymbols, Collection quoteSymbols, Collection barSymbols, Collection newsSymbols); - - /** - * Gets all the currently subscribed control {@link MarketDataMessageType}s. - * - * @return a {@link Collection} of {@link MarketDataMessageType}s - */ - Collection subscribedControls(); - - /** - * Gets all the currently subscribed symbols for trade updates. - * - * @return a {@link Collection} of {@link String}s - */ - Collection subscribedTrades(); - - /** - * Gets all the currently subscribed symbols for quote updates. - * - * @return a {@link Collection} of {@link String}s - */ - Collection subscribedQuotes(); - - /** - * Gets all the currently subscribed symbols for bar updates. - * - * @return a {@link Collection} of {@link String}s - */ - Collection subscribedBars(); - - Collection subscribedNews(); - -} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/crypto/CryptoMarketDataWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/crypto/CryptoMarketDataWebsocket.java deleted file mode 100644 index 27168332..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/crypto/CryptoMarketDataWebsocket.java +++ /dev/null @@ -1,27 +0,0 @@ -package net.jacobpeterson.alpaca.websocket.marketdata.crypto; - -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.realtime.bar.CryptoBarMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.realtime.quote.CryptoQuoteMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.realtime.trade.CryptoTradeMessage; -import net.jacobpeterson.alpaca.websocket.marketdata.MarketDataWebsocket; -import okhttp3.OkHttpClient; - -/** - * {@link CryptoMarketDataWebsocket} is a {@link MarketDataWebsocket} for - * Realtime - * Crypto Market Data - */ -public class CryptoMarketDataWebsocket extends MarketDataWebsocket { - - /** - * Instantiates a new {@link CryptoMarketDataWebsocket}. - * - * @param okHttpClient the {@link OkHttpClient} - * @param keyID the key ID - * @param secretKey the secret key - */ - public CryptoMarketDataWebsocket(OkHttpClient okHttpClient, String keyID, String secretKey) { - super(okHttpClient, "v1beta3/crypto/us", "Crypto", keyID, secretKey, CryptoTradeMessage.class, - CryptoQuoteMessage.class, CryptoBarMessage.class); - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/news/NewsMarketDataWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/news/NewsMarketDataWebsocket.java deleted file mode 100644 index 9d2c2620..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/news/NewsMarketDataWebsocket.java +++ /dev/null @@ -1,15 +0,0 @@ -package net.jacobpeterson.alpaca.websocket.marketdata.news; - -import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.bar.BarMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.quote.QuoteMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.trade.TradeMessage; -import net.jacobpeterson.alpaca.websocket.marketdata.MarketDataWebsocket; -import okhttp3.OkHttpClient; - -public class NewsMarketDataWebsocket extends MarketDataWebsocket { - public NewsMarketDataWebsocket(OkHttpClient okHttpClient, - String keyID, String secretKey) { - super(okHttpClient, "v1beta1/news", "News", keyID, secretKey, TradeMessage.class, - QuoteMessage.class, BarMessage.class); - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/stock/StockMarketDataWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/stock/StockMarketDataWebsocket.java deleted file mode 100644 index 4b897f0d..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/stock/StockMarketDataWebsocket.java +++ /dev/null @@ -1,30 +0,0 @@ -package net.jacobpeterson.alpaca.websocket.marketdata.stock; - -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.bar.StockBarMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.quote.StockQuoteMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.trade.StockTradeMessage; -import net.jacobpeterson.alpaca.model.properties.DataAPIType; -import net.jacobpeterson.alpaca.websocket.marketdata.MarketDataWebsocket; -import okhttp3.OkHttpClient; - -/** - * {@link StockMarketDataWebsocket} is a {@link MarketDataWebsocket} for - * Realtime - * Stock Market Data - */ -public class StockMarketDataWebsocket extends MarketDataWebsocket { - - /** - * Instantiates a new {@link StockMarketDataWebsocket}. - * - * @param okHttpClient the {@link OkHttpClient} - * @param dataAPIType the {@link DataAPIType} - * @param keyID the key ID - * @param secretKey the secret key - */ - public StockMarketDataWebsocket(OkHttpClient okHttpClient, DataAPIType dataAPIType, - String keyID, String secretKey) { - super(okHttpClient, "v2/" + dataAPIType.toString(), "Stock", keyID, secretKey, StockTradeMessage.class, - StockQuoteMessage.class, StockBarMessage.class); - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingListener.java b/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingListener.java deleted file mode 100644 index 3ebf714d..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingListener.java +++ /dev/null @@ -1,11 +0,0 @@ -package net.jacobpeterson.alpaca.websocket.streaming; - -import net.jacobpeterson.alpaca.model.endpoint.streaming.StreamingMessage; -import net.jacobpeterson.alpaca.model.endpoint.streaming.enums.StreamingMessageType; -import net.jacobpeterson.alpaca.websocket.AlpacaWebsocketMessageListener; - -/** - * {@link StreamingListener} defines a listener interface for {@link StreamingWebsocket} messages. - */ -public interface StreamingListener extends AlpacaWebsocketMessageListener { -} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocket.java deleted file mode 100644 index 2fccb63e..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocket.java +++ /dev/null @@ -1,257 +0,0 @@ -package net.jacobpeterson.alpaca.websocket.streaming; - -import com.google.common.collect.Iterables; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.google.gson.JsonPrimitive; -import net.jacobpeterson.alpaca.model.endpoint.streaming.StreamingMessage; -import net.jacobpeterson.alpaca.model.endpoint.streaming.authorization.AuthorizationData; -import net.jacobpeterson.alpaca.model.endpoint.streaming.authorization.AuthorizationMessage; -import net.jacobpeterson.alpaca.model.endpoint.streaming.enums.StreamingMessageType; -import net.jacobpeterson.alpaca.model.endpoint.streaming.listening.ListeningMessage; -import net.jacobpeterson.alpaca.model.endpoint.streaming.trade.TradeUpdateMessage; -import net.jacobpeterson.alpaca.model.properties.EndpointAPIType; -import net.jacobpeterson.alpaca.websocket.AlpacaWebsocket; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.WebSocket; -import okio.ByteString; -import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; -import static com.google.common.base.Predicates.not; -import static net.jacobpeterson.alpaca.util.gson.GsonUtil.GSON; - -/** - * {@link StreamingWebsocket} is an {@link AlpacaWebsocket} implementation and provides the - * {@link StreamingWebsocketInterface} interface for - * Streaming - */ -public class StreamingWebsocket extends AlpacaWebsocket - implements StreamingWebsocketInterface { - - private static final Logger LOGGER = LoggerFactory.getLogger(StreamingWebsocket.class); - private static final String STREAM_ELEMENT_KEY = "stream"; - private static final List SUBSCRIBABLE_STREAMING_MESSAGE_TYPES = Collections.singletonList( - StreamingMessageType.TRADE_UPDATES); - - /** - * Creates a {@link HttpUrl} for {@link StreamingWebsocket} with the given alpacaSubdomain. - * - * @param endpointAPIType the {@link EndpointAPIType} - * - * @return a {@link HttpUrl} - */ - @SuppressWarnings("UnnecessaryDefault") - private static HttpUrl createWebsocketURL(EndpointAPIType endpointAPIType) { - return new HttpUrl.Builder() - .scheme("https") // HttpUrl.Builder doesn't recognize "wss" scheme, but "https" works fine - .host((switch (endpointAPIType) { - case LIVE -> "api"; - case PAPER -> "paper-api"; - default -> throw new UnsupportedOperationException(); - }) + ".alpaca.markets") - .addPathSegment("stream") - .build(); - } - - private final Set listenedStreamMessageTypes; - - /** - * Instantiates a new {@link StreamingWebsocket}. - * - * @param okHttpClient the {@link OkHttpClient} - * @param endpointAPIType the {@link EndpointAPIType} - * @param keyID the key ID - * @param secretKey the secret key - * @param oAuthToken the OAuth token - */ - public StreamingWebsocket(OkHttpClient okHttpClient, EndpointAPIType endpointAPIType, - String keyID, String secretKey, String oAuthToken) { - super(okHttpClient, createWebsocketURL(endpointAPIType), "Streaming", keyID, secretKey, oAuthToken); - - listenedStreamMessageTypes = new HashSet<>(); - } - - @Override - protected void cleanupState() { - super.cleanupState(); - - listenedStreamMessageTypes.clear(); - } - - @Override - protected void onConnection() { - sendAuthenticationMessage(); - } - - @Override - protected void onReconnection() { - sendAuthenticationMessage(); - if (waitForAuthorization(5, TimeUnit.SECONDS)) { - streams(Iterables.toArray(listenedStreamMessageTypes, StreamingMessageType.class)); - } - } - - @Override - protected void sendAuthenticationMessage() { - // Ensures that 'authenticationMessageFuture' exists - getAuthorizationFuture(); - - /* Authentication message format: - * { - * "action": "authenticate", - * "data": { - * "key_id": "{YOUR_API_KEY_ID}", - * "secret_key": "{YOUR_API_SECRET_KEY}" - * } - * } - */ - - JsonObject authObject = new JsonObject(); - authObject.addProperty("action", "authenticate"); - - JsonObject authData = new JsonObject(); - - if (useOAuth) { - authData.addProperty("oauth_token", oAuthToken); - } else { - authData.addProperty("key_id", keyID); - authData.addProperty("secret_key", secretKey); - } - - authObject.add("data", authData); - - LOGGER.info("{} websocket sending authentication message...", websocketName); - websocket.send(authObject.toString()); - } - - // This websocket uses binary frames and not text frames. - @Override - public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString byteString) { - final String message = byteString.utf8(); - LOGGER.trace("{}", message); - - JsonElement messageElement = JsonParser.parseString(message); - checkState(messageElement instanceof JsonObject, "Message must be a JsonObject! Received: %s", messageElement); - - JsonObject messageObject = messageElement.getAsJsonObject(); - JsonElement streamElement = messageObject.get(STREAM_ELEMENT_KEY); - checkState(streamElement instanceof JsonPrimitive, - "Message must contain %s element! Received: %s", STREAM_ELEMENT_KEY, messageElement); - - StreamingMessageType streamingMessageType = GSON.fromJson(streamElement, StreamingMessageType.class); - checkNotNull(streamingMessageType, "StreamingMessageType not found in message: %s", messageObject); - - StreamingMessage streamingMessage; - switch (streamingMessageType) { - case AUTHORIZATION: - streamingMessage = GSON.fromJson(messageObject, AuthorizationMessage.class); - - authenticated = isAuthorizationMessageSuccess((AuthorizationMessage) streamingMessage); - - if (!authenticated) { - LOGGER.error("{} websocket not authenticated! Received: {}.", websocketName, streamingMessage); - } else { - LOGGER.info("{} websocket authenticated.", websocketName); - } - - if (authenticationMessageFuture != null) { - authenticationMessageFuture.complete(authenticated); - } - break; - case LISTENING: - streamingMessage = GSON.fromJson(messageObject, ListeningMessage.class); - - // Remove all 'StreamingMessageType's that are no longer listened to and add new ones - List currentTypes = ((ListeningMessage) streamingMessage).getData().getStreams(); - currentTypes.stream() - .filter(not(listenedStreamMessageTypes::contains)) - .forEach(listenedStreamMessageTypes::remove); - listenedStreamMessageTypes.addAll(currentTypes); - break; - case TRADE_UPDATES: - streamingMessage = GSON.fromJson(messageObject, TradeUpdateMessage.class); - break; - default: - throw new UnsupportedOperationException(); - } - - if (listenedStreamMessageTypes.contains(streamingMessageType)) { - callListener(streamingMessageType, streamingMessage); - } - } - - /** - * Returns true if {@link AuthorizationMessage} states success. - * - * @param authorizationMessage the {@link AuthorizationMessage} - * - * @return a boolean - */ - private boolean isAuthorizationMessageSuccess(AuthorizationMessage authorizationMessage) { - AuthorizationData authorizationData = authorizationMessage.getData(); - return authorizationData.getStatus().equalsIgnoreCase("authorized") && - authorizationData.getAction().equalsIgnoreCase("authenticate"); - } - - @Override - public void streams(StreamingMessageType... streamingMessageTypes) { - if (streamingMessageTypes == null || streamingMessageTypes.length == 0) { - return; - } - - // Add all non-subscribable 'streamingMessageTypes' before connecting or sending websocket message - Arrays.stream(streamingMessageTypes) - .filter(not(SUBSCRIBABLE_STREAMING_MESSAGE_TYPES::contains)) - .forEach(listenedStreamMessageTypes::add); - - // Stream request format: - // { - // "action": "listen", - // "data": { - // "streams": ["trade_updates"] - // } - // } - - JsonObject requestObject = new JsonObject(); - requestObject.addProperty("action", "listen"); - - JsonArray streamsArray = new JsonArray(); - Arrays.stream(streamingMessageTypes) - .filter(SUBSCRIBABLE_STREAMING_MESSAGE_TYPES::contains) - .forEach((type) -> streamsArray.add(type.toString())); - - if (streamsArray.isEmpty()) { - return; - } else if (!isConnected()) { - throw new IllegalStateException("This websocket must be connected before subscribing to streams!"); - } - - JsonObject dataObject = new JsonObject(); - dataObject.add("streams", streamsArray); - - requestObject.add("data", dataObject); - - websocket.send(requestObject.toString()); - LOGGER.info("Requested streams: {}.", streamsArray); - } - - @Override - public Collection streams() { - return new HashSet<>(listenedStreamMessageTypes); - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocketInterface.java b/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocketInterface.java deleted file mode 100644 index 44f0042e..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocketInterface.java +++ /dev/null @@ -1,26 +0,0 @@ -package net.jacobpeterson.alpaca.websocket.streaming; - -import net.jacobpeterson.alpaca.model.endpoint.streaming.enums.StreamingMessageType; -import net.jacobpeterson.alpaca.websocket.AlpacaWebsocketInterface; - -import java.util.Collection; - -/** - * {@link StreamingWebsocketInterface} is an {@link AlpacaWebsocketInterface} for a {@link StreamingWebsocket}. - */ -public interface StreamingWebsocketInterface extends AlpacaWebsocketInterface { - - /** - * Sets the {@link StreamingMessageType}s for this stream to the given streamingMessageTypes. - * - * @param streamingMessageTypes the {@link StreamingMessageType}s - */ - void streams(StreamingMessageType... streamingMessageTypes); - - /** - * Gets all the currently subscribed {@link StreamingMessageType}s. - * - * @return a {@link Collection} of {@link StreamingMessageType} - */ - Collection streams(); -} diff --git a/src/main/resources/alpaca.default.properties b/src/main/resources/alpaca.default.properties deleted file mode 100644 index e3629268..00000000 --- a/src/main/resources/alpaca.default.properties +++ /dev/null @@ -1,2 +0,0 @@ -endpoint_api_type:paper -data_api_type:iex From a084b1b5ebfc4bc4557148611c859a62f0284c95 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Sun, 3 Mar 2024 14:30:43 -0800 Subject: [PATCH 59/84] WIP implement Broker 'events' API SSE capability --- .gitignore | 2 - README.md | 27 +-- build.gradle | 126 ++++++------ gradle/wrapper/.gitignore | 1 - .../net/jacobpeterson/alpaca/AlpacaAPI.java | 76 ++++---- .../jacobpeterson/alpaca/AlpacaBrokerAPI.java | 19 +- .../alpaca/AlpacaMarketDataAPI.java | 3 +- .../alpaca/rest/broker/EventsApiSSE.java | 180 ++++++++++++++++++ ...> market_data_api_stream_source_type.json} | 2 +- .../alpaca/util/sse/SSEListener.java | 38 ++++ .../alpaca/util/sse/SSEListenerAdapter.java | 33 ++++ .../alpaca/util/sse/SSERequest.java | 36 ++++ 12 files changed, 416 insertions(+), 127 deletions(-) delete mode 100644 gradle/wrapper/.gitignore create mode 100644 src/main/java/net/jacobpeterson/alpaca/rest/broker/EventsApiSSE.java rename src/main/java/net/jacobpeterson/alpaca/util/apitype/{market_data_api_source_type.json => market_data_api_stream_source_type.json} (56%) create mode 100644 src/main/java/net/jacobpeterson/alpaca/util/sse/SSEListener.java create mode 100644 src/main/java/net/jacobpeterson/alpaca/util/sse/SSEListenerAdapter.java create mode 100644 src/main/java/net/jacobpeterson/alpaca/util/sse/SSERequest.java diff --git a/.gitignore b/.gitignore index 985d4c2c..12a5664a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,3 @@ build/ .settings .DS_Store .idea/ -*.properties -!*.default.properties diff --git a/README.md b/README.md index 94b7e833..c5ceae7f 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@

# Overview -This library is a Java client implementation of the Alpaca API. Alpaca lets you trade with algorithms, connect with apps, and build services all with a commission-free trading API for stocks, crypto, and options. This library uses the [Alpaca OpenAPI Specifications](https://docs.alpaca.markets/v1.1/openapi) to generate REST API clients and implements the websocket streaming interface using [OkHttp](https://square.github.io/okhttp/). This library is community developed and if you have any questions, please ask them on [Github Discussions](https://github.com/Petersoj/alpaca-java/discussions), the [Alpaca Slack #dev-alpaca-java channel](https://alpaca.markets/slack), or on the [Alpaca Forums](https://forum.alpaca.markets/). +This library is a Java client implementation of the Alpaca API. Alpaca lets you trade with algorithms, connect with apps, and build services all with a commission-free trading API for stocks, crypto, and options. This library uses the [Alpaca OpenAPI Specifications](https://docs.alpaca.markets/v1.1/openapi) to generate clients for the REST API with the [OkHttp](https://square.github.io/okhttp/) library, but implements the websocket and SSE streaming interface using a custom implementation with [OkHttp](https://square.github.io/okhttp/) library. This library is community developed and if you have any questions, please ask them on [Github Discussions](https://github.com/Petersoj/alpaca-java/discussions), the [Alpaca Slack #dev-alpaca-java channel](https://alpaca.markets/slack), or on the [Alpaca Forums](https://forum.alpaca.markets/). Give this repository a star ⭐ if it helped you build a trading algorithm in Java! @@ -28,17 +28,7 @@ If you are using Maven as your build tool, add the following dependency to your ``` -Note that you don't have to use the Maven Central artifacts and instead can just install a clone of this project to your local Maven repository as shown in the [Building](#building) section. - -# Configuration -Creating an `alpaca.properties` file on the classpath (e.g. in `src/main/resources/alpaca.properties`) with the following format allows you to easily load properties using the `AlpacaAPI` default constructor: -``` -key_id = -secret_key = -endpoint_api_type = -data_api_type = -``` -The default values for `alpaca.properties` can be found [here](src/main/resources/alpaca.default.properties). +Note that you don't have to use the Maven Central artifacts. Instead, you can clone this repository, build this project, and install the artifacts to your local Maven repository as shown in the [Building](#building) section. # Logger For logging, this library uses [SLF4j](http://www.slf4j.org/) which serves as an interface for various logging frameworks. This enables you to use whatever logging framework you would like. However, if you do not add a logging framework as a dependency in your project, the console will output a message stating that SLF4j is defaulting to a no-operation (NOP) logger implementation. To enable logging, add a logging framework of your choice as a dependency to your project such as [Logback](http://logback.qos.ch/), [Log4j 2](http://logging.apache.org/log4j/2.x/index.html), [SLF4j-simple](http://www.slf4j.org/manual.html), or [Apache Commons Logging](https://commons.apache.org/proper/commons-logging/). @@ -76,11 +66,7 @@ AlpacaAPI alpacaAPI = AlpacaAPI.builder() .build(); ``` -Note that this library uses [OkHttp](https://square.github.io/okhttp/) as its HTTP client library which creates background threads to service requests. These threads persist even if the main thread exists so if you want to destroy these threads when you're done using [`AlpacaAPI`](src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java) so your program can exit without calling `System.exit()`, use the following snippet: -```java -alpacaAPI.getOkHttpClient().dispatcher().executorService().shutdown(); -alpacaAPI.getOkHttpClient().connectionPool().evictAll(); -``` +Note that this library uses [OkHttp](https://square.github.io/okhttp/) as its HTTP client library which creates background threads to service requests via a connection pool. These threads persist even if the main thread exits, so if you want to destroy these threads when you're done using [`AlpacaAPI`](src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java), call `alpacaAPI.closeOkHttpClient();`. See the [OkHttpClient Documentation](https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/#shutdown-isnt-necessary) for more information. @@ -532,16 +518,15 @@ To build this project yourself, clone this repository and run: To install the built artifacts to your local Maven repository on your machine (the `~/.m2/` directory), run: ``` -./gradlew install +./gradlew publishToMavenLocal ``` # TODO - Implement Unit Testing for REST API and Websocket streaming (both live and mocked) -- Implement Broker API websockets # Contributing Contributions are welcome! -When creating a Pull Request, keep the following in mind: +Do the following before starting your Pull Request: 1. Create a new branch in your forked repository for your feature or bug fix instead of committing directly to the `master` branch in your fork. -2. Use the `dev` branch in this repository as the base branch in your Pull Request. +2. Use the `dev` branch as the base branch in your Pull Request. diff --git a/build.gradle b/build.gradle index b13ea565..1c8e27b1 100644 --- a/build.gradle +++ b/build.gradle @@ -48,6 +48,7 @@ dependencies { // OkHttp implementation group: "com.squareup.okhttp3", name: "okhttp", version: "5.0.0-alpha.12" + implementation group: "com.squareup.okhttp3", name: "okhttp-sse", version: "5.0.0-alpha.12" implementation group: "com.squareup.okhttp3", name: "logging-interceptor", version: "5.0.0-alpha.12" // Libraries used by the OpenAPI generated client libraries @@ -69,6 +70,9 @@ java { } javadocJar.dependsOn generatePOJOs sourcesJar.dependsOn generatePOJOs +jar.dependsOn compileJava +javadocJar.dependsOn compileJava +sourcesJar.dependsOn compileJava [compileJava, compileTestJava]*.options*.encoding = "UTF-8" @@ -93,19 +97,10 @@ final File specDownloadPath = new File(project.layout.buildDirectory.get().getAs final File generatedClientLibrariesPath = new File(project.layout.buildDirectory.get().getAsFile(), "/generated/openapi/") -final def generateOpenAPIClientsTasks = tasks.register("generateOpenAPIClients") { - doLast { - fixOpenAPIGeneratedClientIssues(specIDsOfFileNames, generatedClientLibrariesPath) - } -} -compileJava.dependsOn generateOpenAPIClientsTasks -jar.dependsOn generateOpenAPIClientsTasks -javadocJar.dependsOn generateOpenAPIClientsTasks -sourcesJar.dependsOn generateOpenAPIClientsTasks - def downloadOpenAPISpecFilesTask = tasks.register("downloadOpenAPISpecFiles") { configure { outputs.dir(specDownloadPath) + outputs.cacheIf { true } } doLast { @@ -131,6 +126,16 @@ def downloadOpenAPISpecFilesTask = tasks.register("downloadOpenAPISpecFiles") { } } +final def generateOpenAPIClientsTask = tasks.register("generateOpenAPIClients") { + it.onlyIf { + !downloadOpenAPISpecFilesTask.get().state.upToDate && + downloadOpenAPISpecFilesTask.get().state.failure == null && + !downloadOpenAPISpecFilesTask.get().state.skipped && + downloadOpenAPISpecFilesTask.get().state.executed + } +} +compileJava.dependsOn generateOpenAPIClientsTask + for (def specFileName : specIDsOfFileNames.keySet()) { final def specName = specFileName.replace(".json", "") final def inputSpecFile = new File(specDownloadPath, specFileName) @@ -151,16 +156,10 @@ for (def specFileName : specIDsOfFileNames.keySet()) { downloadOpenAPISpecFilesTask } - it.onlyIf { - !downloadOpenAPISpecFilesTask.get().state.upToDate && - downloadOpenAPISpecFilesTask.get().state.failure == null && - !downloadOpenAPISpecFilesTask.get().state.skipped && - downloadOpenAPISpecFilesTask.get().state.executed - } - configure { inputs.file(inputSpecFile) outputs.dir(outputDirectory) + outputs.cacheIf { true } } generatorName.set("java") @@ -186,52 +185,64 @@ for (def specFileName : specIDsOfFileNames.keySet()) { generateApiTests.set(false) generateApiDocumentation.set(false) } - generateOpenAPIClientsTasks.configure { + generateOpenAPIClientsTask.configure { dependsOn generateTask } } -static void fixOpenAPIGeneratedClientIssues(Map specIDsOfFileNames, File generatedClientLibrariesPath) { - // TODO remove these manual fixes once OpenAPI fixes these issues - - final def sourceFilesPath = "src/main/java/net/jacobpeterson/alpaca/openapi" - - // Fix broker spec generated client issues with discriminators - // on some enums: https://github.com/OpenAPITools/openapi-generator/issues/806 - final def brokerTransferDataJavaFile = new File(generatedClientLibrariesPath, - "broker/${sourceFilesPath}/broker/model/TransferData.java") - brokerTransferDataJavaFile.text = brokerTransferDataJavaFile.text - .replace("this.transferType = this.getClass().getSimpleName();", "") - final def brokerTransferResourceJavaFile = new File(generatedClientLibrariesPath, - "broker/${sourceFilesPath}/broker/model/TransferResource.java") - brokerTransferResourceJavaFile.text = brokerTransferResourceJavaFile.text - .replace("this.type = this.getClass().getSimpleName();", "") - - // Remove the JSON validation that the generated client models always run (there isn't an option to - // disable it for some reason) - specIDsOfFileNames.keySet().stream().map { it.replace(".json", "") }.forEach { - for (File modelFile : Paths.get(generatedClientLibrariesPath.getPath(), - it, sourceFilesPath, it.replace("-", ""), "model").toFile().listFiles()) { - modelFile.text = modelFile.text - .replaceAll("/\\*\\*\\n {2}\\* Validates the JSON Element and throws an exception if issues found" + - "[\\s\\S]*?\\*/", "") - .replace("public static void validateJsonElement(JsonElement jsonElement) throws IOException {", - "public static boolean validate = false;\n public static void " + - "validateJsonElement(JsonElement jsonElement) throws IOException {\n" + - " if (!validate) return;") - } +final def fixOpenAPIGeneratedClientIssuesTask = tasks.register("fixOpenAPIGeneratedClientIssues") { + it.onlyIf { + !generateOpenAPIClientsTask.get().state.upToDate && + generateOpenAPIClientsTask.get().state.failure == null && + !generateOpenAPIClientsTask.get().state.skipped && + generateOpenAPIClientsTask.get().state.executed } - // Remove the necessary authentication header logic from the generate client's ApiClient - specIDsOfFileNames.keySet().stream().map { it.replace(".json", "") }.forEach { - final def apiClientFile = Paths.get(generatedClientLibrariesPath.getPath(), it, sourceFilesPath, - it.replace("-", ""), "ApiClient.java").toFile() - apiClientFile.text = apiClientFile.text - .replaceAll("// Setup authentications \\(key: authentication name, value: authentication\\)" + - "[\\s\\S]*?authentications\\);", "") - .replaceAll("for \\(String authName : authNames\\)[\\s\\S]*?uri\\);\\n. {7}}", "") + doLast { + // TODO remove these manual fixes once OpenAPI fixes these issues + + final def sourceFilesPath = "src/main/java/net/jacobpeterson/alpaca/openapi" + + // Fix broker spec generated client issues with discriminators + // on some enums: https://github.com/OpenAPITools/openapi-generator/issues/806 + final def brokerTransferDataJavaFile = new File(generatedClientLibrariesPath, + "broker/${sourceFilesPath}/broker/model/TransferData.java") + brokerTransferDataJavaFile.text = brokerTransferDataJavaFile.text + .replace("this.transferType = this.getClass().getSimpleName();", "") + final def brokerTransferResourceJavaFile = new File(generatedClientLibrariesPath, + "broker/${sourceFilesPath}/broker/model/TransferResource.java") + brokerTransferResourceJavaFile.text = brokerTransferResourceJavaFile.text + .replace("this.type = this.getClass().getSimpleName();", "") + + // Remove the JSON validation that the generated client models always run (there isn't an option to + // disable it for some reason) + specIDsOfFileNames.keySet().stream().map { it.replace(".json", "") }.forEach { + for (File modelFile : Paths.get(generatedClientLibrariesPath.getPath(), + it, sourceFilesPath, it.replace("-", ""), "model").toFile().listFiles()) { + modelFile.text = modelFile.text + .replaceAll("/\\*\\*\\n {2}\\* Validates the JSON Element and throws an exception if issues " + + "found[\\s\\S]*?\\*/", "") + .replace("public static void validateJsonElement(JsonElement jsonElement) throws IOException {", + "public static boolean validate = false;\n public static void " + + "validateJsonElement(JsonElement jsonElement) throws IOException {\n" + + " if (!validate) return;") + } + } + + // Remove the necessary authentication header logic from the generate client's ApiClient + specIDsOfFileNames.keySet().stream().map { it.replace(".json", "") }.forEach { + final def apiClientFile = Paths.get(generatedClientLibrariesPath.getPath(), it, sourceFilesPath, + it.replace("-", ""), "ApiClient.java").toFile() + apiClientFile.text = apiClientFile.text + .replaceAll("// Setup authentications \\(key: authentication name, value: authentication\\)" + + "[\\s\\S]*?authentications\\);", "") + .replaceAll("for \\(String authName : authNames\\)[\\s\\S]*?uri\\);\\n. {7}}", "") + } } } +generateOpenAPIClientsTask.configure { + finalizedBy fixOpenAPIGeneratedClientIssuesTask +} // // END Alpaca OpenAPI Specification (OAS) client generation @@ -299,11 +310,6 @@ signing { sign publishing.publications.mavenJava } -// Used to prevent accidental publication of Alpaca keys -processResources { - exclude("alpaca.properties") -} - // // END Publishing // diff --git a/gradle/wrapper/.gitignore b/gradle/wrapper/.gitignore deleted file mode 100644 index 351625b1..00000000 --- a/gradle/wrapper/.gitignore +++ /dev/null @@ -1 +0,0 @@ -!gradle-wrapper.properties diff --git a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java index 577324ec..6e6f2da3 100644 --- a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java +++ b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java @@ -1,7 +1,7 @@ package net.jacobpeterson.alpaca; import net.jacobpeterson.alpaca.model.util.apitype.BrokerAPIEndpointType; -import net.jacobpeterson.alpaca.model.util.apitype.MarketDataAPISourceType; +import net.jacobpeterson.alpaca.model.util.apitype.MarketDataAPIStreamSourceType; import net.jacobpeterson.alpaca.model.util.apitype.TraderAPIEndpointType; import okhttp3.OkHttpClient; import okhttp3.logging.HttpLoggingInterceptor; @@ -9,8 +9,9 @@ import org.slf4j.LoggerFactory; import static net.jacobpeterson.alpaca.model.util.apitype.BrokerAPIEndpointType.SANDBOX; -import static net.jacobpeterson.alpaca.model.util.apitype.MarketDataAPISourceType.IEX; +import static net.jacobpeterson.alpaca.model.util.apitype.MarketDataAPIStreamSourceType.IEX; import static net.jacobpeterson.alpaca.model.util.apitype.TraderAPIEndpointType.PAPER; +import static okhttp3.logging.HttpLoggingInterceptor.Level.BODY; /** * {@link AlpacaAPI} is the main class used to interface with the various Alpaca API endpoints. If you are using the @@ -30,7 +31,7 @@ public class AlpacaAPI { private final String traderSecretKey; private final String traderOAuthToken; private final TraderAPIEndpointType traderAPIEndpointType; - private final MarketDataAPISourceType marketDataAPISourceType; + private final MarketDataAPIStreamSourceType marketDataAPIStreamSourceType; private final String brokerAPIKey; private final String brokerAPISecret; private final BrokerAPIEndpointType brokerAPIEndpointType; @@ -44,14 +45,14 @@ public class AlpacaAPI { * Instantiates a new {@link AlpacaAPI}. Use this constructor if you are using the Trading or Market Data APIs for a * single Alpaca account. * - * @param traderKeyID the Trader key ID - * @param traderSecretKey the Trader secret key - * @param traderAPIEndpointType the {@link TraderAPIEndpointType} - * @param marketDataAPISourceType the {@link MarketDataAPISourceType} + * @param traderKeyID the Trader key ID + * @param traderSecretKey the Trader secret key + * @param traderAPIEndpointType the {@link TraderAPIEndpointType} + * @param marketDataAPIStreamSourceType the {@link MarketDataAPIStreamSourceType} */ public AlpacaAPI(String traderKeyID, String traderSecretKey, TraderAPIEndpointType traderAPIEndpointType, - MarketDataAPISourceType marketDataAPISourceType) { - this(traderKeyID, traderSecretKey, null, traderAPIEndpointType, marketDataAPISourceType, null, null, null, + MarketDataAPIStreamSourceType marketDataAPIStreamSourceType) { + this(traderKeyID, traderSecretKey, null, traderAPIEndpointType, marketDataAPIStreamSourceType, null, null, null, null); } @@ -59,16 +60,16 @@ public AlpacaAPI(String traderKeyID, String traderSecretKey, TraderAPIEndpointTy * Instantiates a new {@link AlpacaAPI}. Use this constructor if you are using the Trading or Market Data APIs for a * single Alpaca account and a custom {@link OkHttpClient} instance. * - * @param traderKeyID the Trader key ID - * @param traderSecretKey the Trader secret key - * @param traderAPIEndpointType the {@link TraderAPIEndpointType} - * @param marketDataAPISourceType the {@link MarketDataAPISourceType} - * @param okHttpClient an existing {@link OkHttpClient} or null to create a new default - * instance + * @param traderKeyID the Trader key ID + * @param traderSecretKey the Trader secret key + * @param traderAPIEndpointType the {@link TraderAPIEndpointType} + * @param marketDataAPIStreamSourceType the {@link MarketDataAPIStreamSourceType} + * @param okHttpClient an existing {@link OkHttpClient} or null to create a new + * default instance */ public AlpacaAPI(String traderKeyID, String traderSecretKey, TraderAPIEndpointType traderAPIEndpointType, - MarketDataAPISourceType marketDataAPISourceType, OkHttpClient okHttpClient) { - this(traderKeyID, traderSecretKey, null, traderAPIEndpointType, marketDataAPISourceType, null, null, null, + MarketDataAPIStreamSourceType marketDataAPIStreamSourceType, OkHttpClient okHttpClient) { + this(traderKeyID, traderSecretKey, null, traderAPIEndpointType, marketDataAPIStreamSourceType, null, null, null, okHttpClient); } @@ -125,27 +126,28 @@ public AlpacaAPI(String brokerAPIKey, String brokerAPISecret, BrokerAPIEndpointT /** * Instantiates a new {@link AlpacaAPI}. * - * @param traderKeyID the Trader key ID - * @param traderSecretKey the Trader secret key - * @param traderOAuthToken the Trader OAuth token - * @param traderAPIEndpointType the {@link TraderAPIEndpointType} - * @param marketDataAPISourceType the {@link MarketDataAPISourceType} - * @param brokerAPIKey the Broker API key - * @param brokerAPISecret the Broker API secret - * @param brokerAPIEndpointType the {@link BrokerAPIEndpointType} - * @param okHttpClient an existing {@link OkHttpClient} or null to create a new default - * instance + * @param traderKeyID the Trader key ID + * @param traderSecretKey the Trader secret key + * @param traderOAuthToken the Trader OAuth token + * @param traderAPIEndpointType the {@link TraderAPIEndpointType} + * @param marketDataAPIStreamSourceType the {@link MarketDataAPIStreamSourceType} + * @param brokerAPIKey the Broker API key + * @param brokerAPISecret the Broker API secret + * @param brokerAPIEndpointType the {@link BrokerAPIEndpointType} + * @param okHttpClient an existing {@link OkHttpClient} or null to create a new + * default instance */ public AlpacaAPI(String traderKeyID, String traderSecretKey, String traderOAuthToken, TraderAPIEndpointType traderAPIEndpointType, - MarketDataAPISourceType marketDataAPISourceType, + MarketDataAPIStreamSourceType marketDataAPIStreamSourceType, String brokerAPIKey, String brokerAPISecret, BrokerAPIEndpointType brokerAPIEndpointType, OkHttpClient okHttpClient) { this.traderKeyID = traderKeyID; this.traderSecretKey = traderSecretKey; this.traderOAuthToken = traderOAuthToken; this.traderAPIEndpointType = traderAPIEndpointType != null ? traderAPIEndpointType : PAPER; - this.marketDataAPISourceType = marketDataAPISourceType != null ? marketDataAPISourceType : IEX; + this.marketDataAPIStreamSourceType = marketDataAPIStreamSourceType != null ? + marketDataAPIStreamSourceType : IEX; this.brokerAPIKey = brokerAPIKey; this.brokerAPISecret = brokerAPISecret; this.brokerAPIEndpointType = brokerAPIEndpointType != null ? brokerAPIEndpointType : SANDBOX; @@ -154,13 +156,23 @@ public AlpacaAPI(String traderKeyID, String traderSecretKey, if (okHttpClient == null) { OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); if (LOGGER.isDebugEnabled()) { - clientBuilder.addInterceptor(new HttpLoggingInterceptor(LOGGER::debug)); + final HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(LOGGER::debug); + loggingInterceptor.setLevel(BODY); + clientBuilder.addInterceptor(loggingInterceptor); } okHttpClient = clientBuilder.build(); } this.okHttpClient = okHttpClient; } + /** + * Closes the {@link OkHttpClient}. + */ + public void closeOkHttpClient() { + okHttpClient.dispatcher().executorService().shutdown(); + okHttpClient.connectionPool().evictAll(); + } + /** * Gets the {@link OkHttpClient}. * @@ -226,7 +238,7 @@ public static final class Builder { private String traderSecretKey; private String traderOAuthToken; private TraderAPIEndpointType traderAPIEndpointType; - private MarketDataAPISourceType marketDataAPISourceType; + private MarketDataAPIStreamSourceType marketDataAPISourceType; private String brokerAPIKey; private String brokerAPISecret; private BrokerAPIEndpointType brokerAPIEndpointType; @@ -254,7 +266,7 @@ public Builder withTraderAPIEndpointType(TraderAPIEndpointType traderAPIEndpoint return this; } - public Builder withMarketDataAPISourceType(MarketDataAPISourceType marketDataAPISourceType) { + public Builder withMarketDataAPIStreamSourceType(MarketDataAPIStreamSourceType marketDataAPISourceType) { this.marketDataAPISourceType = marketDataAPISourceType; return this; } diff --git a/src/main/java/net/jacobpeterson/alpaca/AlpacaBrokerAPI.java b/src/main/java/net/jacobpeterson/alpaca/AlpacaBrokerAPI.java index 7d9851b5..a4aa0c56 100644 --- a/src/main/java/net/jacobpeterson/alpaca/AlpacaBrokerAPI.java +++ b/src/main/java/net/jacobpeterson/alpaca/AlpacaBrokerAPI.java @@ -9,7 +9,6 @@ import net.jacobpeterson.alpaca.openapi.broker.api.CorporateActionsApi; import net.jacobpeterson.alpaca.openapi.broker.api.CountryInfoApi; import net.jacobpeterson.alpaca.openapi.broker.api.DocumentsApi; -import net.jacobpeterson.alpaca.openapi.broker.api.EventsApi; import net.jacobpeterson.alpaca.openapi.broker.api.FundingApi; import net.jacobpeterson.alpaca.openapi.broker.api.FundingWalletsApi; import net.jacobpeterson.alpaca.openapi.broker.api.JournalsApi; @@ -20,6 +19,7 @@ import net.jacobpeterson.alpaca.openapi.broker.api.ReportingApi; import net.jacobpeterson.alpaca.openapi.broker.api.TradingApi; import net.jacobpeterson.alpaca.openapi.broker.api.WatchlistApi; +import net.jacobpeterson.alpaca.rest.broker.EventsApiSSE; import okhttp3.OkHttpClient; import static com.google.common.base.Preconditions.checkNotNull; @@ -39,7 +39,7 @@ public class AlpacaBrokerAPI { private CorporateActionsApi corporateActions; private CountryInfoApi countryInfo; private DocumentsApi documents; - private EventsApi events; + private EventsApiSSE events; private FundingApi funding; private FundingWalletsApi fundingWallets; private JournalsApi journals; @@ -60,8 +60,8 @@ public class AlpacaBrokerAPI { * @param okHttpClient an existing {@link OkHttpClient} or null to create a new default * instance */ - @SuppressWarnings("UnnecessaryDefault") - AlpacaBrokerAPI(String brokerAPIKey, String brokerAPISecret, BrokerAPIEndpointType brokerAPIEndpointType, + @SuppressWarnings("UnnecessaryDefault") AlpacaBrokerAPI(String brokerAPIKey, String brokerAPISecret, + BrokerAPIEndpointType brokerAPIEndpointType, OkHttpClient okHttpClient) { checkNotNull(brokerAPIKey); checkNotNull(brokerAPISecret); @@ -74,7 +74,8 @@ public class AlpacaBrokerAPI { case PRODUCTION -> 1; default -> throw new UnsupportedOperationException(); }); - apiClient.addDefaultHeader("Authorization", createBrokerAPIAuthKey(brokerAPIKey, brokerAPISecret)); + apiClient.addDefaultHeader("Authorization", "Basic " + + createBrokerAPIAuthKey(brokerAPIKey, brokerAPISecret)); } /** @@ -171,13 +172,13 @@ public synchronized DocumentsApi documents() { } /** - * Gets the {@link EventsApi}. Lazily instantiated. + * Gets the {@link EventsApiSSE}. Lazily instantiated. * - * @return the {@link EventsApi} + * @return the {@link EventsApiSSE} */ - public synchronized EventsApi events() { + public synchronized EventsApiSSE events() { if (events == null) { - events = new EventsApi(apiClient); + events = new EventsApiSSE(apiClient); } return events; } diff --git a/src/main/java/net/jacobpeterson/alpaca/AlpacaMarketDataAPI.java b/src/main/java/net/jacobpeterson/alpaca/AlpacaMarketDataAPI.java index 1538581a..65682a4e 100644 --- a/src/main/java/net/jacobpeterson/alpaca/AlpacaMarketDataAPI.java +++ b/src/main/java/net/jacobpeterson/alpaca/AlpacaMarketDataAPI.java @@ -53,7 +53,8 @@ public class AlpacaMarketDataAPI { apiClient.addDefaultHeader("APCA-API-KEY-ID", traderKeyID); apiClient.addDefaultHeader("APCA-API-SECRET-KEY", traderSecretKey); } else { - apiClient.addDefaultHeader("Authorization", createBrokerAPIAuthKey(brokerAPIKey, brokerAPISecret)); + apiClient.addDefaultHeader("Authorization", "Basic " + + createBrokerAPIAuthKey(brokerAPIKey, brokerAPISecret)); } } diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/broker/EventsApiSSE.java b/src/main/java/net/jacobpeterson/alpaca/rest/broker/EventsApiSSE.java new file mode 100644 index 00000000..da0ad98d --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/rest/broker/EventsApiSSE.java @@ -0,0 +1,180 @@ +package net.jacobpeterson.alpaca.rest.broker; + +import com.google.gson.reflect.TypeToken; +import net.jacobpeterson.alpaca.openapi.broker.ApiClient; +import net.jacobpeterson.alpaca.openapi.broker.ApiException; +import net.jacobpeterson.alpaca.openapi.broker.api.EventsApi; +import net.jacobpeterson.alpaca.openapi.broker.model.AccountStatusEvent; +import net.jacobpeterson.alpaca.openapi.broker.model.JournalStatusEvent; +import net.jacobpeterson.alpaca.openapi.broker.model.SubscribeToAdminActionSSE200ResponseInner; +import net.jacobpeterson.alpaca.openapi.broker.model.TradeUpdateEvent; +import net.jacobpeterson.alpaca.openapi.broker.model.TradeUpdateEventV2; +import net.jacobpeterson.alpaca.openapi.broker.model.TransferStatusEvent; +import net.jacobpeterson.alpaca.util.sse.SSEListener; +import net.jacobpeterson.alpaca.util.sse.SSERequest; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.sse.EventSource; +import okhttp3.sse.EventSourceListener; +import okhttp3.sse.EventSources; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Type; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.util.List; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static net.jacobpeterson.alpaca.openapi.broker.JSON.getGson; + +/** + * {@link EventsApiSSE} add SSE support to {@link EventsApi}. + */ +public class EventsApiSSE { + + private final EventsApi eventsAPI; + private final EventSource.Factory eventSourceFactory; + + /** + * Instantiates a new {@link EventsApiSSE}. + * + * @param apiClient the api client + */ + public EventsApiSSE(ApiClient apiClient) { + eventsAPI = new EventsApi(apiClient); + eventSourceFactory = EventSources.createFactory(apiClient.getHttpClient().newBuilder() // Shallow clone + .readTimeout(0, SECONDS) + .writeTimeout(0, SECONDS) + .build()); + } + + /** + * See {@link EventsApi#getV1EventsNta(String, String, String, Integer, Integer, String, String, Boolean)}. + * + * @return a {@link SSERequest} + */ + public SSERequest subscribeToV1EventsNta(String id, String since, String until, Integer sinceId, Integer untilId, + String sinceUlid, String untilUlid, Boolean includePreprocessing, + SSEListener> sseListener) throws ApiException { // TODO OpenAPI response type is broken + final Request request = eventsAPI.getV1EventsNtaCall(id, since, until, sinceId, untilId, sinceUlid, untilUlid, + includePreprocessing, null).request(); + return new SSERequest(eventSourceFactory.newEventSource(request, createEventSourceListener(sseListener, + new TypeToken>() {}.getType()))); // TODO OpenAPI response type is broken + } + + /** + * See {@link EventsApi#subscribeToAdminActionSSE(OffsetDateTime, OffsetDateTime, String, String)}. + * + * @return a {@link SSERequest} + */ + public SSERequest subscribeToAdminAction(OffsetDateTime since, OffsetDateTime until, String sinceId, + String untilId, SSEListener> sseListener) throws ApiException { + final Request request = eventsAPI.subscribeToAdminActionSSECall(since, until, sinceId, untilId, null).request(); + return new SSERequest(eventSourceFactory.newEventSource(request, createEventSourceListener(sseListener, + new TypeToken>() {}.getType()))); + } + + /** + * See + * {@link EventsApi#subscribeToJournalStatusSSE(OffsetDateTime, OffsetDateTime, Integer, Integer, String, String, + * String)}. + * + * @return a {@link SSERequest} + */ + public SSERequest subscribeToJournalStatus(OffsetDateTime since, OffsetDateTime until, Integer sinceId, + Integer untilId, String sinceUlid, String untilUlid, String id, + SSEListener> sseListener) throws ApiException { + final Request request = eventsAPI.subscribeToJournalStatusSSECall(since, until, sinceId, untilId, sinceUlid, + untilUlid, id, null).request(); + return new SSERequest(eventSourceFactory.newEventSource(request, createEventSourceListener(sseListener, + new TypeToken>() {}.getType()))); + } + + /** + * See {@link EventsApi#subscribeToTradeSSE(OffsetDateTime, OffsetDateTime, Integer, Integer, String, String)}. + * + * @return a {@link SSERequest} + */ + public SSERequest subscribeToTrade(OffsetDateTime since, OffsetDateTime until, Integer sinceId, + Integer untilId, String sinceUlid, String untilUlid, SSEListener> sseListener) + throws ApiException { + final Request request = eventsAPI.subscribeToTradeSSECall(since, until, sinceId, untilId, sinceUlid, untilUlid, + null).request(); + return new SSERequest(eventSourceFactory.newEventSource(request, createEventSourceListener(sseListener, + new TypeToken>() {}.getType()))); + } + + /** + * See {@link EventsApi#subscribeToTradeV2SSE(OffsetDateTime, OffsetDateTime, String, String)}. + * + * @return a {@link SSERequest} + */ + public SSERequest subscribeToTradeV2(OffsetDateTime since, OffsetDateTime until, String sinceId, + String untilId, SSEListener> sseListener) throws ApiException { + final Request request = eventsAPI.subscribeToTradeV2SSECall(since, until, sinceId, untilId, null).request(); + return new SSERequest(eventSourceFactory.newEventSource(request, createEventSourceListener(sseListener, + new TypeToken>() {}.getType()))); + } + + /** + * See + * {@link EventsApi#subscribeToTransferStatusSSE(OffsetDateTime, OffsetDateTime, Integer, Integer, String, + * String)}. + * + * @return a {@link SSERequest} + */ + public SSERequest subscribeToTransferStatus(OffsetDateTime since, OffsetDateTime until, Integer sinceId, + Integer untilId, String sinceUlid, String untilUlid, SSEListener> sseListener) + throws ApiException { + final Request request = eventsAPI.subscribeToTransferStatusSSECall(since, until, sinceId, untilId, sinceUlid, + untilUlid, null).request(); + return new SSERequest(eventSourceFactory.newEventSource(request, createEventSourceListener(sseListener, + new TypeToken>() {}.getType()))); + } + + /** + * See + * {@link EventsApi#suscribeToAccountStatusSSE(LocalDate, LocalDate, Integer, Integer, String, String, String)}. + * + * @return a {@link SSERequest} + */ + public SSERequest subscribeToAccountStatus(LocalDate since, LocalDate until, Integer sinceId, Integer untilId, + String sinceUlid, String untilUlid, String id, SSEListener> sseListener) + throws ApiException { + final Request request = eventsAPI.suscribeToAccountStatusSSECall(since, until, sinceId, untilId, sinceUlid, + untilUlid, id, null).request(); + return new SSERequest(eventSourceFactory.newEventSource(request, createEventSourceListener(sseListener, + new TypeToken>() {}.getType()))); + } + + private EventSourceListener createEventSourceListener(SSEListener sseListener, Type responseTypeToken) { + return new EventSourceListener() { + @Override + public void onClosed(@NotNull EventSource eventSource) { + sseListener.onClose(); + } + + @Override + public void onEvent(@NotNull EventSource eventSource, @Nullable String id, @Nullable String type, + @NotNull String data) { + sseListener.onEvent(getGson().fromJson(data, responseTypeToken)); + } + + @Override + public void onFailure(@NotNull EventSource eventSource, @Nullable Throwable throwable, + @Nullable Response response) { + if (throwable != null && throwable.getMessage().equals("canceled")) { + sseListener.onClose(); + return; + } + sseListener.onFailure(throwable, response); + } + + @Override + public void onOpen(@NotNull EventSource eventSource, @NotNull Response response) { + sseListener.onOpen(); + } + }; + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/util/apitype/market_data_api_source_type.json b/src/main/java/net/jacobpeterson/alpaca/util/apitype/market_data_api_stream_source_type.json similarity index 56% rename from src/main/java/net/jacobpeterson/alpaca/util/apitype/market_data_api_source_type.json rename to src/main/java/net/jacobpeterson/alpaca/util/apitype/market_data_api_stream_source_type.json index e0f74cdc..97b87531 100644 --- a/src/main/java/net/jacobpeterson/alpaca/util/apitype/market_data_api_source_type.json +++ b/src/main/java/net/jacobpeterson/alpaca/util/apitype/market_data_api_stream_source_type.json @@ -1,6 +1,6 @@ { "type": "string", - "javaName": "MarketDataAPISourceType", + "javaName": "MarketDataAPIStreamSourceType", "enum": [ "iex", "sip" diff --git a/src/main/java/net/jacobpeterson/alpaca/util/sse/SSEListener.java b/src/main/java/net/jacobpeterson/alpaca/util/sse/SSEListener.java new file mode 100644 index 00000000..9ca01253 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/util/sse/SSEListener.java @@ -0,0 +1,38 @@ +package net.jacobpeterson.alpaca.util.sse; + +import okhttp3.Response; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * {@link SSEListener} is a listener interface for server-sent events (SSE). + * + * @param the data type + */ +public interface SSEListener { + + /** + * Called on SSE request open. + */ + void onOpen(); + + /** + * Called on SSE request close. + */ + void onClose(); + + /** + * Called on SSE request failure. + * + * @param throwable the {@link Throwable} + * @param response the {@link Response} + */ + void onFailure(@Nullable Throwable throwable, @Nullable Response response); + + /** + * Called on SSE request data receive event. + * + * @param data the {@link T} data + */ + void onEvent(@NotNull T data); +} diff --git a/src/main/java/net/jacobpeterson/alpaca/util/sse/SSEListenerAdapter.java b/src/main/java/net/jacobpeterson/alpaca/util/sse/SSEListenerAdapter.java new file mode 100644 index 00000000..492e9e0f --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/util/sse/SSEListenerAdapter.java @@ -0,0 +1,33 @@ +package net.jacobpeterson.alpaca.util.sse; + +import okhttp3.Response; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link SSEListenerAdapter} and adapter for {@link SSEListener}. + * + * @param the data type + */ +public class SSEListenerAdapter implements SSEListener { + + private static final Logger LOGGER = LoggerFactory.getLogger(SSEListenerAdapter.class); + + public void onOpen() { + LOGGER.info("SSE connection opened."); + } + + public void onClose() { + LOGGER.info("SSE connection closed."); + } + + public void onFailure(@Nullable Throwable throwable, @Nullable Response response) { + LOGGER.error("SSE connection failed! {}", response, throwable); + } + + public void onEvent(@NotNull T data) { + LOGGER.info("SSE event received: {}", data); + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/util/sse/SSERequest.java b/src/main/java/net/jacobpeterson/alpaca/util/sse/SSERequest.java new file mode 100644 index 00000000..65e5b96e --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/util/sse/SSERequest.java @@ -0,0 +1,36 @@ +package net.jacobpeterson.alpaca.util.sse; + +import okhttp3.sse.EventSource; + +/** + * {@link SSERequest} is a {@link EventSource} wrapper class. + */ +public class SSERequest { + + private final EventSource eventSource; + + /** + * Instantiates a new {@link SSERequest}. + * + * @param eventSource the {@link EventSource} + */ + public SSERequest(EventSource eventSource) { + this.eventSource = eventSource; + } + + /** + * Closes the internal {@link EventSource}. + */ + public void close() { + eventSource.cancel(); + } + + /** + * Gets the internal {@link EventSource}. + * + * @return the {@link EventSource} + */ + public EventSource getInternalEventSource() { + return eventSource; + } +} From c37268aa6d86390335c54967d6425a475f0368ed Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Mon, 4 Mar 2024 10:13:20 -0800 Subject: [PATCH 60/84] WIP add updated trades streaming functionality --- README.md | 473 +----------------- build.gradle | 8 + .../net/jacobpeterson/alpaca/AlpacaAPI.java | 16 + .../jacobpeterson/alpaca/AlpacaBrokerAPI.java | 4 +- .../authorization/authorization_message.json | 13 - .../enums/streaming_message_type.json | 9 - .../streaming/listening/listening_data.json | 10 - .../listening/listening_message.json | 13 - .../endpoint/streaming/streaming_message.json | 10 - .../streaming/trade/trade_update_message.json | 13 - .../alpaca/rest/broker/EventsApiSSE.java | 37 +- .../alpaca/util/sse/SSEListener.java | 14 +- .../alpaca/util/sse/SSEListenerAdapter.java | 8 +- .../alpaca/websocket/AlpacaWebsocket.java | 287 +++++++++++ .../websocket/AlpacaWebsocketInterface.java | 82 +++ .../AlpacaWebsocketMessageListener.java | 20 + .../AlpacaWebsocketStateListener.java | 33 ++ .../websocket/trades/TradesListener.java | 11 + .../trades/TradesListenerAdapter.java | 19 + .../websocket/trades/TradesWebsocket.java | 219 ++++++++ .../trades/TradesWebsocketInterface.java | 26 + .../authorization/authorization_data.json | 1 - .../authorization/authorization_message.json | 12 + .../model/listening/listening_data.json | 9 + .../model/listening/listening_message.json | 12 + .../trades/model/trades_stream_message.json | 9 + .../model/trades_stream_message_type.json | 8 + .../model/tradeupdate}/trade_update.json | 22 +- .../tradeupdate}/trade_update_event.json | 7 +- .../tradeupdate/trade_update_message.json | 12 + 30 files changed, 845 insertions(+), 572 deletions(-) delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/authorization/authorization_message.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/enums/streaming_message_type.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/listening/listening_data.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/listening/listening_message.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/streaming_message.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/trade/trade_update_message.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketInterface.java create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketMessageListener.java create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketStateListener.java create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesListener.java create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesListenerAdapter.java create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesWebsocket.java create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesWebsocketInterface.java rename src/main/java/net/jacobpeterson/alpaca/{endpoint/streaming => websocket/trades/model}/authorization/authorization_data.json (69%) create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/authorization/authorization_message.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/listening/listening_data.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/listening/listening_message.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/trades_stream_message.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/trades_stream_message_type.json rename src/main/java/net/jacobpeterson/alpaca/{endpoint/streaming/trade => websocket/trades/model/tradeupdate}/trade_update.json (52%) rename src/main/java/net/jacobpeterson/alpaca/{endpoint/streaming/trade/enums => websocket/trades/model/tradeupdate}/trade_update_event.json (83%) create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/tradeupdate/trade_update_message.json diff --git a/README.md b/README.md index c5ceae7f..afb356b9 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@

Logo

GitHub Repository - Maven Central - Javadocs + Maven Central + Javadocs GitHub

# Overview -This library is a Java client implementation of the Alpaca API. Alpaca lets you trade with algorithms, connect with apps, and build services all with a commission-free trading API for stocks, crypto, and options. This library uses the [Alpaca OpenAPI Specifications](https://docs.alpaca.markets/v1.1/openapi) to generate clients for the REST API with the [OkHttp](https://square.github.io/okhttp/) library, but implements the websocket and SSE streaming interface using a custom implementation with [OkHttp](https://square.github.io/okhttp/) library. This library is community developed and if you have any questions, please ask them on [Github Discussions](https://github.com/Petersoj/alpaca-java/discussions), the [Alpaca Slack #dev-alpaca-java channel](https://alpaca.markets/slack), or on the [Alpaca Forums](https://forum.alpaca.markets/). +This library is a Java client implementation of the Alpaca API. Alpaca lets you trade with algorithms, connect with apps, and build services all with a commission-free trading API for stocks, crypto, and options. This library uses the [Alpaca OpenAPI Specifications](https://docs.alpaca.markets/v1.1/openapi) to generate clients for the REST API with the [OkHttp](https://square.github.io/okhttp/) library, but implements the websocket and SSE streaming interface using a custom implementation with the [OkHttp](https://square.github.io/okhttp/) library. This library is community developed and if you have any questions, please ask them on [Github Discussions](https://github.com/Petersoj/alpaca-java/discussions), the [Alpaca Slack #dev-alpaca-java channel](https://alpaca.markets/slack), or on the [Alpaca Forums](https://forum.alpaca.markets/). Give this repository a star ⭐ if it helped you build a trading algorithm in Java! @@ -34,480 +34,33 @@ Note that you don't have to use the Maven Central artifacts. Instead, you can cl For logging, this library uses [SLF4j](http://www.slf4j.org/) which serves as an interface for various logging frameworks. This enables you to use whatever logging framework you would like. However, if you do not add a logging framework as a dependency in your project, the console will output a message stating that SLF4j is defaulting to a no-operation (NOP) logger implementation. To enable logging, add a logging framework of your choice as a dependency to your project such as [Logback](http://logback.qos.ch/), [Log4j 2](http://logging.apache.org/log4j/2.x/index.html), [SLF4j-simple](http://www.slf4j.org/manual.html), or [Apache Commons Logging](https://commons.apache.org/proper/commons-logging/). # Examples -Note that the examples below are not exhaustive. Refer to the [Javadoc](https://javadoc.io/doc/net.jacobpeterson/alpaca-java) for all classes and method signatures. +Note that the examples below are not exhaustive. Refer to the [Javadoc](https://javadoc.io/doc/net.jacobpeterson.alpaca/alpaca-java) for all classes and method signatures. ## [`AlpacaAPI`](src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java) -The [`AlpacaAPI`](src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java) class contains several instances of various [`AlpacaEndpoints`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AlpacaEndpoint.java) and [`AlpacaWebsockets`](src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java) to interface with Alpaca. You will generally only need one instance of this class in your application. Note that many methods inside the various [`AlpacaEndpoints`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AlpacaEndpoint.java) allow `null` to be passed in as a parameter if it is optional. +[`AlpacaAPI`](src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java) is the main class used to interface with the various Alpaca API endpoints. If you are using the Trading or Market Data APIs for a single Alpaca account or if you are using the Broker API, you will generally only need one instance of this class. However, if you are using the Trading API with OAuth to act on behalf of an Alpaca account, this class is optimized so that it can be instantiated quickly, especially when an existing `OkHttpClient` is given in the constructor. Additionally, all API endpoint instances are instantiated lazily. This class is thread-safe. -The Alpaca API specification is located [here](https://docs.alpaca.markets/api-documentation/api-v2/) and the [`AlpacaAPI`](src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java) Javadoc is located [here](https://javadoc.io/doc/net.jacobpeterson/alpaca-java/latest/net/jacobpeterson/alpaca/AlpacaAPI.html). +The Alpaca API documentation is located [here](https://docs.alpaca.markets/) and the [`AlpacaAPI`](src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java) Javadoc is located [here](https://javadoc.io/doc/net.jacobpeterson.alpaca/alpaca-java/latest/net/jacobpeterson/alpaca/AlpacaAPI.html). Example usage: ```java -// This constructor uses the 'alpaca.properties' file on the classpath for configuration -AlpacaAPI alpacaAPI = new AlpacaAPI(); - -// This constructor passes in a 'keyID' and 'secretKey' and uses the endpoint API type and data API -// type defined in the 'alpaca.properties' file (which default to 'paper' and 'iex' respectively) -String keyID = ""; -String secretKey = ""; -AlpacaAPI alpacaAPI = new AlpacaAPI(keyID, secretKey); - -// This constructor passes in a 'keyID' and 'secretKey' and uses the passed in endpoint API type -// and data API type (which are 'LIVE' and 'SIP' respectively in this example) -AlpacaAPI alpacaAPI = new AlpacaAPI(keyID, secretKey, EndpointAPIType.LIVE, DataAPIType.SIP); - -// This constructor is for OAuth tokens -String oAuthToken = ""; -AlpacaAPI alpacaAPI = new AlpacaAPI(oAuthToken); - -// This approach uses a builder pattern. Attributes not provided explicitly are retrieved from 'alpaca.properties' file -AlpacaAPI alpacaAPI = AlpacaAPI.builder() - .withEndpointAPIType(EndpointAPIType.PAPER) - .build(); +// TODO ``` Note that this library uses [OkHttp](https://square.github.io/okhttp/) as its HTTP client library which creates background threads to service requests via a connection pool. These threads persist even if the main thread exits, so if you want to destroy these threads when you're done using [`AlpacaAPI`](src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java), call `alpacaAPI.closeOkHttpClient();`. -See the [OkHttpClient Documentation](https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/#shutdown-isnt-necessary) for more information. - -## [`AlpacaClientException`](src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClientException.java) -[`AlpacaClientException`](src/main/java/net/jacobpeterson/alpaca/rest/AlpacaClientException.java) is thrown anytime an exception occurs when using various [`AlpacaEndpoints`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/AlpacaEndpoint.java). It should be caught and handled within your trading algorithm application accordingly. - -## [`AccountEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/account/AccountEndpoint.java) -The Account API serves important information related to an account, including account status, funds available for trade, funds available for withdrawal, and various flags relevant to an account's ability to trade. - -Example usage: +## Trading API ```java -try { - // Get 'Account' information and print it out - Account account = alpacaAPI.account().get(); - System.out.println(account); -} catch (AlpacaClientException exception) { - exception.printStackTrace(); -} -``` - -## [`CryptoMarketDataEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/crypto/CryptoMarketDataEndpoint.java) -Alpaca provides cryptocurrency data from multiple venues/exchanges, namely: Coinbase, ErisX, and FTX. - -Example usage: -```java -try { - // Get BTCUSD 50 one-hour bars starting on 12/18/2021 from Coinbase and print them out - CryptoBarsResponse btcBarsResponse = alpacaAPI.cryptoMarketData().getBars( - "BTCUSD", - Arrays.asList(Exchange.COINBASE), - ZonedDateTime.of(2021, 12, 18, 0, 0, 0, 0, ZoneId.of("America/New_York")), - 50, - null, - 1, - BarTimePeriod.HOUR); - btcBarsResponse.getBars().forEach(System.out::println); - - // Get the Best Bid and Offer across multiple exchanges (XBBO) and print it out - XbboResponse etcXBBO = alpacaAPI.cryptoMarketData().getXBBO( - "ETHUSD", - null); - System.out.println(etcXBBO); -} catch (AlpacaClientException exception) { - exception.printStackTrace(); -} +// TODO ``` -## [`StockMarketDataEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/marketdata/stock/StockMarketDataEndpoint.java) -The Data API v2 provides market data through an easy-to-use API for historical stock market data. - -Example usage: +## Market Data API ```java -try { - // Get AAPL one hour, split-adjusted bars from 7/6/2021 market open - // to 7/8/2021 market close from the SIP feed and print them out - StockBarsResponse aaplBarsResponse = alpacaAPI.stockMarketData().getBars( - "AAPL", - ZonedDateTime.of(2021, 7, 6, 9, 30, 0, 0, ZoneId.of("America/New_York")), - ZonedDateTime.of(2021, 7, 8, 12 + 4, 0, 0, 0, ZoneId.of("America/New_York")), - null, - null, - 1, - BarTimePeriod.HOUR, - BarAdjustment.SPLIT, - BarFeed.SIP); - aaplBarsResponse.getBars().forEach(System.out::println); - - // Get AAPL first 10 trades on 7/8/2021 at market open and print them out - StockTradesResponse aaplTradesResponse = alpacaAPI.stockMarketData().getTrades( - "AAPL", - ZonedDateTime.of(2021, 7, 8, 9, 30, 0, 0, ZoneId.of("America/New_York")), - ZonedDateTime.of(2021, 7, 8, 9, 31, 0, 0, ZoneId.of("America/New_York")), - 10, - null); - aaplTradesResponse.getTrades().forEach(System.out::println); - - // Print out latest AAPL trade - Trade latestAAPLTrade = alpacaAPI.stockMarketData().getLatestTrade("AAPL").getTrade(); - System.out.printf("Latest AAPL Trade: %s\n", latestAAPLTrade); - - // Print out snapshot of AAPL, GME, and TSLA - Map snapshots = alpacaAPI.stockMarketData() - .getSnapshots(Arrays.asList("AAPL", "GME", "TSLA")); - snapshots.forEach((symbol, snapshot) -> - System.out.printf("Symbol: %s\nSnapshot: %s\n\n", symbol, snapshot)); -} catch (AlpacaClientException exception) { - exception.printStackTrace(); -} +// TODO ``` -## [`OrdersEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/orders/OrdersEndpoint.java) -The Orders API allows a user to monitor, place, and cancel their orders with Alpaca. - -Example usage: +## Broker API ```java -try { - // Print all the 'Order's of AAPL and TSLA until 7/6/2021 in - // ascending (oldest to newest) order - List orders = alpacaAPI.orders().get( - CurrentOrderStatus.ALL, - null, - null, - ZonedDateTime.of(2021, 7, 6, 0, 0, 0, 0, ZoneId.of("America/New_York")), - SortDirection.ASCENDING, - true, - Arrays.asList("AAPL", "TSLA")); - orders.forEach(System.out::println); - - // Request a new limit order for TSLA for 100 shares at a limit price - // of $653.00 and TIF of DAY. - Order tslaLimitOrder = alpacaAPI.orders().requestLimitOrder( - "TSLA", - 100, - OrderSide.BUY, - OrderTimeInForce.DAY, - 653.00, - false); - System.out.printf("Requested %s %s order at %s\n", - tslaLimitOrder.getSymbol(), - tslaLimitOrder.getType(), - tslaLimitOrder.getSubmittedAt()); - - // Check if TSLA limit order has filled 5 seconds later and if it hasn't - // replace it with a higher limit order so it fills! - Thread.sleep(5000); - tslaLimitOrder = alpacaAPI.orders().get(tslaLimitOrder.getId(), false); - if (tslaLimitOrder.getFilledAt() == null && tslaLimitOrder.getStatus().equals(OrderStatus.NEW)) { - Order replacedOrder = alpacaAPI.orders().replace( - tslaLimitOrder.getId(), - 100, - OrderTimeInForce.DAY, - 655.00, - null, null, null); - System.out.printf("Replaced TSLA order: %s\n", replacedOrder); - } - - // Cancel all open orders after 2 seconds - Thread.sleep(2000); - List cancelledOrders = alpacaAPI.orders().cancelAll(); - cancelledOrders.forEach((cancelledOrder) -> System.out.printf("Cancelled: %s\n", cancelledOrder)); - - // Request a new fractional market order for 0.5 shares of GME - alpacaAPI.orders().requestFractionalMarketOrder("GME", 0.5, OrderSide.BUY); - // Request a new notional market order for $25 worth of GME shares - alpacaAPI.orders().requestNotionalMarketOrder("GME", 25d, OrderSide.BUY); -} catch (AlpacaClientException | InterruptedException exception) { - exception.printStackTrace(); -} -``` - -## [`PositionsEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/positions/PositionsEndpoint.java) -The Positions API provides information about an account's current open positions. - -Example usage: -```java -try { - // Close 50% of all open TSLA and AAPL positions - List openPositions = alpacaAPI.positions().get(); - for (Position openPosition : openPositions) { - if (openPosition.getSymbol().equals("TSLA") || openPosition.getSymbol().equals("AAPL")) { - Order closePositionOrder = alpacaAPI.positions() - .close(openPosition.getSymbol(), null, 50d); - System.out.printf("Closing 50%% of %s position: %s\n", - openPosition.getSymbol(), closePositionOrder); - } - } -} catch (AlpacaClientException exception) { - exception.printStackTrace(); -} -``` - -## [`AssetsEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/assets/AssetsEndpoint.java) -The Assets API serves as the master list of assets available for trade and data consumption from Alpaca. - -Example usage: -```java -try { - // Print out a CSV of all active US Equity 'Asset's - List activeUSEquities = alpacaAPI.assets().get(AssetStatus.ACTIVE, AssetClass.US_EQUITY); - System.out.println(activeUSEquities - .stream().map(Asset::getSymbol) - .collect(Collectors.joining(", "))); - - // Print out TSLA 'Asset' information - Asset tslaAsset = alpacaAPI.assets().getBySymbol("TSLA"); - System.out.println(tslaAsset); -} catch (AlpacaClientException exception) { - exception.printStackTrace(); -} -``` - -## [`WatchlistEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/watchlist/WatchlistEndpoint.java) -The Watchlist API provides CRUD operation for the account's watchlist. - -Example usage: -```java -try { - // Print out CSV all 'Watchlist' names - List watchlists = alpacaAPI.watchlist().get(); - System.out.println(watchlists.stream() - .map(Watchlist::getName) - .collect(Collectors.joining(", "))); - - // Create a watchlist for day trading with TSLA and AAPL initially - Watchlist dayTradeWatchlist = alpacaAPI.watchlist().create("Day Trade", "TSLA", "AAPL"); - - // Remove TSLA and add MSFT to 'dayTradeWatchlist' - alpacaAPI.watchlist().removeSymbol(dayTradeWatchlist.getId(), "TSLA"); - alpacaAPI.watchlist().addAsset(dayTradeWatchlist.getId(), "MSFT"); - - // Set the updated watchlist variable - dayTradeWatchlist = alpacaAPI.watchlist().get(dayTradeWatchlist.getId()); - - // Print CSV of 'dayTradeWatchlist' 'Asset's - System.out.println(dayTradeWatchlist.getAssets() - .stream().map(Asset::getSymbol) - .collect(Collectors.joining(", "))); - - // Delete the 'dayTradeWatchlist' - alpacaAPI.watchlist().delete(dayTradeWatchlist.getId()); -} catch (AlpacaClientException exception) { - exception.printStackTrace(); -} -``` - -## [`CalendarEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/calendar/CalendarEndpoint.java) -The calendar API serves the full list of market days from 1970 to 2029. - -Example usage: -```java -try { - // Get the 'Calendar's of the week of Christmas 2020 and print them out - List calendar = alpacaAPI.calendar().get( - LocalDate.of(2020, 12, 20), - LocalDate.of(2020, 12, 27)); - calendar.forEach(System.out::println); -} catch (AlpacaClientException exception) { - exception.printStackTrace(); -} -``` - -## [`ClockEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/clock/ClockEndpoint.java) -The clock API serves the current market timestamp, whether the market is currently open, as well as the times of the next market open and close. - -Example usage: -```java -try { - // Get the market 'Clock' and print it out - Clock clock = alpacaAPI.clock().get(); - System.out.println(clock); -} catch (AlpacaClientException exception) { - exception.printStackTrace(); -} -``` - -## [`AccountConfigurationEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/accountconfiguration/AccountConfigurationEndpoint.java) -The Account Configuration API provides custom configurations about your trading account settings. These configurations control various allow you to modify settings to suit your trading needs. - -Example usage: -```java -try { - // Update the 'AccountConfiguration' to block new orders - AccountConfiguration accountConfiguration = alpacaAPI.accountConfiguration().get(); - accountConfiguration.setSuspendTrade(true); - alpacaAPI.accountConfiguration().set(accountConfiguration); -} catch (AlpacaClientException exception) { - exception.printStackTrace(); -} -``` - -## [`AccountActivitiesEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/accountactivities/AccountActivitiesEndpoint.java) -The Account Activities API provides access to a historical record of transaction activities that have impacted your account. - -Example usage: -```java -try { - // Print all order fill and cash deposit 'AccountActivity's on 7/8/2021 - List accountActivities = alpacaAPI.accountActivities().get( - ZonedDateTime.of(2021, 7, 8, 0, 0, 0, 0, ZoneId.of("America/New_York")), - null, - null, - SortDirection.ASCENDING, - null, - null, - ActivityType.FILL, ActivityType.CSD); - for (AccountActivity accountActivity : accountActivities) { - if (accountActivity instanceof TradeActivity) { - System.out.println("TradeActivity: " + (TradeActivity) accountActivity); - } else if (accountActivity instanceof NonTradeActivity) { - System.out.println("NonTradeActivity: " + (NonTradeActivity) accountActivity); - } - } -} catch (AlpacaClientException exception) { - exception.printStackTrace(); -} -``` - -## [`PortfolioHistoryEndpoint`](src/main/java/net/jacobpeterson/alpaca/rest/endpoint/portfoliohistory/PortfolioHistoryEndpoint.java) -The Portfolio History API returns the timeseries data for equity and profit loss information of the account. - -Example usage: -```java -try { - // Get 3 days of one-hour 'PortfolioHistory' on 7/8/2021 and print out its data points - PortfolioHistory portfolioHistory = alpacaAPI.portfolioHistory().get( - 3, - PortfolioPeriodUnit.DAY, - PortfolioTimeFrame.ONE_HOUR, - LocalDate.of(2021, 7, 8), - false); - System.out.printf("Timeframe: %s, Base value: %s \n", - portfolioHistory.getTimeframe(), - portfolioHistory.getBaseValue()); - portfolioHistory.getDataPoints().forEach(System.out::println); -} catch (AlpacaClientException exception) { - exception.printStackTrace(); -} -``` - -## [`StreamingWebsocket`](src/main/java/net/jacobpeterson/alpaca/websocket/streaming/StreamingWebsocket.java) -Alpaca offers websocket streaming of order updates. - -Example usage: -```java -// Add a 'StreamingListener' that simply prints streaming information -StreamingListener streamingListener = (messageType, message) -> - System.out.printf("%s: %s\n", messageType.name(), message); -alpacaAPI.streaming().setListener(streamingListener); - -// Listen 'AuthorizationMessage' and 'ListeningMessage' messages that contain -// information about the stream's current state. Note that these are subscribed -// to before the websocket is connected since these messages usually are sent -// upon websocket connection. -alpacaAPI.streaming().streams(StreamingMessageType.AUTHORIZATION, - StreamingMessageType.LISTENING); - -// Connect the websocket and confirm authentication -alpacaAPI.streaming().connect(); -alpacaAPI.streaming().waitForAuthorization(5, TimeUnit.SECONDS); -if (!alpacaAPI.streaming().isValid()) { - System.out.println("Websocket not valid!"); - return; -} - -// Listen to the 'trade update' streams. -alpacaAPI.streaming().streams(StreamingMessageType.TRADE_UPDATES); - -// Wait a few seconds -Thread.sleep(5000); - -// Manually disconnect the websocket -alpacaAPI.streaming().disconnect(); -``` - -## [`StockMarketDataWebsocket`](src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/stock/StockMarketDataWebsocket.java) -Alpaca's Data API v2 provides websocket streaming for trades, quotes, and minute bars. This helps receive the most up-to-date market information that could help your trading strategy to act upon certain market movement. - -Example usage: -```java -// Add a 'MarketDataListener' that simply prints market data information -MarketDataListener marketDataListener = (messageType, message) -> - System.out.printf("%s: %s\n", messageType.name(), message); -alpacaAPI.stockMarketDataStreaming().setListener(marketDataListener); - -// Listen to 'SubscriptionsMessage', 'SuccessMessage', and 'ErrorMessage' control messages -// that contain information about the stream's current state. Note that these are subscribed -// to before the websocket is connected since these messages usually are sent -// upon websocket connection. -alpacaAPI.stockMarketDataStreaming().subscribeToControl( - MarketDataMessageType.SUCCESS, - MarketDataMessageType.SUBSCRIPTION, - MarketDataMessageType.ERROR); - -// Connect the websocket and confirm authentication -alpacaAPI.stockMarketDataStreaming().connect(); -alpacaAPI.stockMarketDataStreaming().waitForAuthorization(5, TimeUnit.SECONDS); -if (!alpacaAPI.stockMarketDataStreaming().isValid()) { - System.out.println("Websocket not valid!"); - return; -} - -// Listen to AAPL and TSLA trades and all bars via the wildcard operator ('*'). -alpacaAPI.stockMarketDataStreaming().subscribe( - Arrays.asList("AAPL", "TSLA"), - null, - Arrays.asList("*")); - -// Wait a few seconds -Thread.sleep(5000); - -// Manually disconnect the websocket -alpacaAPI.stockMarketDataStreaming().disconnect(); -``` - -## [`CryptoMarketDataWebsocket`](src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/crypto/CryptoMarketDataWebsocket.java) -Alpaca also offers cryptocurrency websocket streaming for trades, quotes, and minute bars. - -The usage is identical to the [`StockMarketDataWebsocket`](https://github.com/Petersoj/alpaca-java/#stockmarketdatawebsocket) usage except that accessing the websocket instance via the `AlpacaAPI` instance is done using: `alpacaAPI.cryptoMarketDataStreaming()` instead of `alpacaAPI.stockMarketDataStreaming()`. - -Example usage: -```java -// The 'CryptoMarketDataWebsocket' setup is identical to the 'StockMarketDataWebsocket' setup - -// Listen to BTCUSD and ETHUSD trades and all bars via the wildcard operator ('*'). -alpacaAPI.cryptoMarketDataStreaming().subscribe( - Arrays.asList("BTCUSD", "ETHUSD"), - null, - Arrays.asList("*")); -``` - -## [`NewsMarketDataWebsocket`](src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/news/NewsMarketDataWebsocket.java) -Alpaca also offers news websocket streaming for symbols. - -The usage is identical to the [`StockMarketDataWebsocket`](https://github.com/Petersoj/alpaca-java/#stockmarketdatawebsocket) usage except that accessing the websocket instance via the `AlpacaAPI` instance is done using: `alpacaAPI.newsMarketDataStreaming()` instead of `alpacaAPI.stockMarketDataStreaming()`. - -Example usage: -```java -MarketDataListener marketDataListener = (messageType, message) -> - System.out.printf("%s: %s\n", messageType.name(), message); - -MarketDataWebsocketInterface newsWebsocket = alpacaAPI.newsMarketDataStreaming(); - -newsWebsocket.setListener(marketDataListener); -newsWebsocket.subscribeToControl( - MarketDataMessageType.SUCCESS, - MarketDataMessageType.SUBSCRIPTION, - MarketDataMessageType.ERROR); - -newsWebsocket.connect(); -newsWebsocket.waitForAuthorization(5, TimeUnit.SECONDS); -if (!newsWebsocket.isValid()) { - System.out.println("Websocket not valid!"); - return; -} -newsWebsocket.subscribe(null, null, null, Arrays.asList("*")); - -// Wait a few seconds -Thread.sleep(5000); - -// Manually disconnect the websocket -newsWebsocket.disconnect(); +// TODO ``` # Building diff --git a/build.gradle b/build.gradle index 1c8e27b1..a0729cd0 100644 --- a/build.gradle +++ b/build.gradle @@ -238,6 +238,14 @@ final def fixOpenAPIGeneratedClientIssuesTask = tasks.register("fixOpenAPIGenera "[\\s\\S]*?authentications\\);", "") .replaceAll("for \\(String authName : authNames\\)[\\s\\S]*?uri\\);\\n. {7}}", "") } + + // Make the 'JSON' classes of the generate clients have a static Gson initializer + specIDsOfFileNames.keySet().stream().map { it.replace(".json", "") }.forEach { + final def jsonClientFile = Paths.get(generatedClientLibrariesPath.getPath(), it, sourceFilesPath, + it.replace("-", ""), "JSON.java").toFile() + jsonClientFile.text = jsonClientFile.text + .replaceAll("\\{\\n {8}GsonBuilder", "static {\n GsonBuilder") + } } } generateOpenAPIClientsTask.configure { diff --git a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java index 6e6f2da3..001aa617 100644 --- a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java +++ b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java @@ -3,6 +3,8 @@ import net.jacobpeterson.alpaca.model.util.apitype.BrokerAPIEndpointType; import net.jacobpeterson.alpaca.model.util.apitype.MarketDataAPIStreamSourceType; import net.jacobpeterson.alpaca.model.util.apitype.TraderAPIEndpointType; +import net.jacobpeterson.alpaca.websocket.trades.TradesWebsocket; +import net.jacobpeterson.alpaca.websocket.trades.TradesWebsocketInterface; import okhttp3.OkHttpClient; import okhttp3.logging.HttpLoggingInterceptor; import org.slf4j.Logger; @@ -40,6 +42,7 @@ public class AlpacaAPI { private AlpacaTraderAPI trader; private AlpacaMarketDataAPI marketData; private AlpacaBrokerAPI broker; + private TradesWebsocket tradesWebsocket; /** * Instantiates a new {@link AlpacaAPI}. Use this constructor if you are using the Trading or Market Data APIs for a @@ -220,6 +223,19 @@ public synchronized AlpacaBrokerAPI broker() { return broker; } + /** + * Gets the {@link TradesWebsocketInterface}. Lazily instantiated. + * + * @return the {@link TradesWebsocketInterface} + */ + public synchronized TradesWebsocketInterface tradesStream() { + if (tradesWebsocket == null) { + tradesWebsocket = new TradesWebsocket(okHttpClient, traderAPIEndpointType, + traderKeyID, traderSecretKey, traderOAuthToken); + } + return tradesWebsocket; + } + /** * Creates a {@link Builder} for {@link AlpacaAPI}. * diff --git a/src/main/java/net/jacobpeterson/alpaca/AlpacaBrokerAPI.java b/src/main/java/net/jacobpeterson/alpaca/AlpacaBrokerAPI.java index a4aa0c56..942d71b3 100644 --- a/src/main/java/net/jacobpeterson/alpaca/AlpacaBrokerAPI.java +++ b/src/main/java/net/jacobpeterson/alpaca/AlpacaBrokerAPI.java @@ -60,8 +60,8 @@ public class AlpacaBrokerAPI { * @param okHttpClient an existing {@link OkHttpClient} or null to create a new default * instance */ - @SuppressWarnings("UnnecessaryDefault") AlpacaBrokerAPI(String brokerAPIKey, String brokerAPISecret, - BrokerAPIEndpointType brokerAPIEndpointType, + @SuppressWarnings("UnnecessaryDefault") + AlpacaBrokerAPI(String brokerAPIKey, String brokerAPISecret, BrokerAPIEndpointType brokerAPIEndpointType, OkHttpClient okHttpClient) { checkNotNull(brokerAPIKey); checkNotNull(brokerAPISecret); diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/authorization/authorization_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/authorization/authorization_message.json deleted file mode 100644 index 5e266609..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/authorization/authorization_message.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "type": "object", - "title": "See Streaming.", - "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.streaming.StreamingMessage" - }, - "properties": { - "data": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.streaming.authorization.AuthorizationData", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.streaming.authorization.AuthorizationData}." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/enums/streaming_message_type.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/enums/streaming_message_type.json deleted file mode 100644 index 454ee643..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/enums/streaming_message_type.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "type": "string", - "title": "See Streaming.", - "enum": [ - "listening", - "authorization", - "trade_updates" - ] -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/listening/listening_data.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/listening/listening_data.json deleted file mode 100644 index eb9d6eb9..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/listening/listening_data.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "type": "object", - "title": "See Streaming.", - "properties": { - "streams": { - "existingJavaType": "java.util.ArrayList", - "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.streaming.enums.StreamingMessageType}s." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/listening/listening_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/listening/listening_message.json deleted file mode 100644 index 24413cb8..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/listening/listening_message.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "type": "object", - "title": "See Streaming.", - "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.streaming.StreamingMessage" - }, - "properties": { - "data": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.streaming.listening.ListeningData", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.streaming.listening.ListeningData}." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/streaming_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/streaming_message.json deleted file mode 100644 index c7799c71..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/streaming_message.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "type": "object", - "title": "See Streaming.", - "properties": { - "stream": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.streaming.enums.StreamingMessageType", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.streaming.enums.StreamingMessageType}." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/trade/trade_update_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/trade/trade_update_message.json deleted file mode 100644 index 014d3d6f..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/trade/trade_update_message.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "type": "object", - "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.streaming.StreamingMessage" - }, - "title": "See Streaming.", - "properties": { - "data": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.streaming.trade.TradeUpdate", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.streaming.trade.TradeUpdate}." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/broker/EventsApiSSE.java b/src/main/java/net/jacobpeterson/alpaca/rest/broker/EventsApiSSE.java index da0ad98d..cff5a03c 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/broker/EventsApiSSE.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/broker/EventsApiSSE.java @@ -23,7 +23,6 @@ import java.lang.reflect.Type; import java.time.LocalDate; import java.time.OffsetDateTime; -import java.util.List; import static java.util.concurrent.TimeUnit.SECONDS; import static net.jacobpeterson.alpaca.openapi.broker.JSON.getGson; @@ -54,13 +53,13 @@ public EventsApiSSE(ApiClient apiClient) { * * @return a {@link SSERequest} */ - public SSERequest subscribeToV1EventsNta(String id, String since, String until, Integer sinceId, Integer untilId, - String sinceUlid, String untilUlid, Boolean includePreprocessing, - SSEListener> sseListener) throws ApiException { // TODO OpenAPI response type is broken + public SSERequest subscribeToNonTradingActivitiesEvents(String id, String since, String until, Integer sinceId, + Integer untilId, String sinceUlid, String untilUlid, Boolean includePreprocessing, + SSEListener sseListener) throws ApiException { // TODO OpenAPI response type is broken final Request request = eventsAPI.getV1EventsNtaCall(id, since, until, sinceId, untilId, sinceUlid, untilUlid, includePreprocessing, null).request(); return new SSERequest(eventSourceFactory.newEventSource(request, createEventSourceListener(sseListener, - new TypeToken>() {}.getType()))); // TODO OpenAPI response type is broken + new TypeToken() {}.getType()))); // TODO OpenAPI response type is broken } /** @@ -69,10 +68,10 @@ public SSERequest subscribeToV1EventsNta(String id, String since, String until, * @return a {@link SSERequest} */ public SSERequest subscribeToAdminAction(OffsetDateTime since, OffsetDateTime until, String sinceId, - String untilId, SSEListener> sseListener) throws ApiException { + String untilId, SSEListener sseListener) throws ApiException { final Request request = eventsAPI.subscribeToAdminActionSSECall(since, until, sinceId, untilId, null).request(); return new SSERequest(eventSourceFactory.newEventSource(request, createEventSourceListener(sseListener, - new TypeToken>() {}.getType()))); + new TypeToken() {}.getType()))); } /** @@ -84,11 +83,11 @@ public SSERequest subscribeToAdminAction(OffsetDateTime since, OffsetDateTime un */ public SSERequest subscribeToJournalStatus(OffsetDateTime since, OffsetDateTime until, Integer sinceId, Integer untilId, String sinceUlid, String untilUlid, String id, - SSEListener> sseListener) throws ApiException { + SSEListener sseListener) throws ApiException { final Request request = eventsAPI.subscribeToJournalStatusSSECall(since, until, sinceId, untilId, sinceUlid, untilUlid, id, null).request(); return new SSERequest(eventSourceFactory.newEventSource(request, createEventSourceListener(sseListener, - new TypeToken>() {}.getType()))); + new TypeToken() {}.getType()))); } /** @@ -97,12 +96,12 @@ public SSERequest subscribeToJournalStatus(OffsetDateTime since, OffsetDateTime * @return a {@link SSERequest} */ public SSERequest subscribeToTrade(OffsetDateTime since, OffsetDateTime until, Integer sinceId, - Integer untilId, String sinceUlid, String untilUlid, SSEListener> sseListener) + Integer untilId, String sinceUlid, String untilUlid, SSEListener sseListener) throws ApiException { final Request request = eventsAPI.subscribeToTradeSSECall(since, until, sinceId, untilId, sinceUlid, untilUlid, null).request(); return new SSERequest(eventSourceFactory.newEventSource(request, createEventSourceListener(sseListener, - new TypeToken>() {}.getType()))); + new TypeToken() {}.getType()))); } /** @@ -111,10 +110,10 @@ public SSERequest subscribeToTrade(OffsetDateTime since, OffsetDateTime until, I * @return a {@link SSERequest} */ public SSERequest subscribeToTradeV2(OffsetDateTime since, OffsetDateTime until, String sinceId, - String untilId, SSEListener> sseListener) throws ApiException { + String untilId, SSEListener sseListener) throws ApiException { final Request request = eventsAPI.subscribeToTradeV2SSECall(since, until, sinceId, untilId, null).request(); return new SSERequest(eventSourceFactory.newEventSource(request, createEventSourceListener(sseListener, - new TypeToken>() {}.getType()))); + new TypeToken() {}.getType()))); } /** @@ -125,12 +124,12 @@ public SSERequest subscribeToTradeV2(OffsetDateTime since, OffsetDateTime until, * @return a {@link SSERequest} */ public SSERequest subscribeToTransferStatus(OffsetDateTime since, OffsetDateTime until, Integer sinceId, - Integer untilId, String sinceUlid, String untilUlid, SSEListener> sseListener) + Integer untilId, String sinceUlid, String untilUlid, SSEListener sseListener) throws ApiException { final Request request = eventsAPI.subscribeToTransferStatusSSECall(since, until, sinceId, untilId, sinceUlid, untilUlid, null).request(); return new SSERequest(eventSourceFactory.newEventSource(request, createEventSourceListener(sseListener, - new TypeToken>() {}.getType()))); + new TypeToken() {}.getType()))); } /** @@ -140,12 +139,12 @@ public SSERequest subscribeToTransferStatus(OffsetDateTime since, OffsetDateTime * @return a {@link SSERequest} */ public SSERequest subscribeToAccountStatus(LocalDate since, LocalDate until, Integer sinceId, Integer untilId, - String sinceUlid, String untilUlid, String id, SSEListener> sseListener) + String sinceUlid, String untilUlid, String id, SSEListener sseListener) throws ApiException { final Request request = eventsAPI.suscribeToAccountStatusSSECall(since, until, sinceId, untilId, sinceUlid, untilUlid, id, null).request(); return new SSERequest(eventSourceFactory.newEventSource(request, createEventSourceListener(sseListener, - new TypeToken>() {}.getType()))); + new TypeToken() {}.getType()))); } private EventSourceListener createEventSourceListener(SSEListener sseListener, Type responseTypeToken) { @@ -158,7 +157,7 @@ public void onClosed(@NotNull EventSource eventSource) { @Override public void onEvent(@NotNull EventSource eventSource, @Nullable String id, @Nullable String type, @NotNull String data) { - sseListener.onEvent(getGson().fromJson(data, responseTypeToken)); + sseListener.onMessage(getGson().fromJson(data, responseTypeToken)); } @Override @@ -168,7 +167,7 @@ public void onFailure(@NotNull EventSource eventSource, @Nullable Throwable thro sseListener.onClose(); return; } - sseListener.onFailure(throwable, response); + sseListener.onError(throwable, response); } @Override diff --git a/src/main/java/net/jacobpeterson/alpaca/util/sse/SSEListener.java b/src/main/java/net/jacobpeterson/alpaca/util/sse/SSEListener.java index 9ca01253..5b28942b 100644 --- a/src/main/java/net/jacobpeterson/alpaca/util/sse/SSEListener.java +++ b/src/main/java/net/jacobpeterson/alpaca/util/sse/SSEListener.java @@ -12,27 +12,27 @@ public interface SSEListener { /** - * Called on SSE request open. + * Called on SSE open. */ void onOpen(); /** - * Called on SSE request close. + * Called on SSE close. */ void onClose(); /** - * Called on SSE request failure. + * Called on SSE error. * * @param throwable the {@link Throwable} * @param response the {@link Response} */ - void onFailure(@Nullable Throwable throwable, @Nullable Response response); + void onError(@Nullable Throwable throwable, @Nullable Response response); /** - * Called on SSE request data receive event. + * Called on SSE message received. * - * @param data the {@link T} data + * @param message the {@link T} message */ - void onEvent(@NotNull T data); + void onMessage(@NotNull T message); } diff --git a/src/main/java/net/jacobpeterson/alpaca/util/sse/SSEListenerAdapter.java b/src/main/java/net/jacobpeterson/alpaca/util/sse/SSEListenerAdapter.java index 492e9e0f..57e33dfb 100644 --- a/src/main/java/net/jacobpeterson/alpaca/util/sse/SSEListenerAdapter.java +++ b/src/main/java/net/jacobpeterson/alpaca/util/sse/SSEListenerAdapter.java @@ -23,11 +23,11 @@ public void onClose() { LOGGER.info("SSE connection closed."); } - public void onFailure(@Nullable Throwable throwable, @Nullable Response response) { - LOGGER.error("SSE connection failed! {}", response, throwable); + public void onError(@Nullable Throwable throwable, @Nullable Response response) { + LOGGER.error("SSE connection error! {}", response, throwable); } - public void onEvent(@NotNull T data) { - LOGGER.info("SSE event received: {}", data); + public void onMessage(@NotNull T message) { + LOGGER.info("SSE message received: {}", message); } } diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java new file mode 100644 index 00000000..d475cc8a --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java @@ -0,0 +1,287 @@ +package net.jacobpeterson.alpaca.websocket; + +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.concurrent.ForkJoinPool.commonPool; + +/** + * {@link AlpacaWebsocket} represents an abstract websocket for Alpaca. + * + * @param the {@link AlpacaWebsocketMessageListener} type parameter + * @param the 'message type' type parameter + * @param the 'message' type parameter + */ +public abstract class AlpacaWebsocket> extends WebSocketListener + implements AlpacaWebsocketInterface { + + /** + * Defines a websocket normal closure code. + * + * @see WebSocket#close(int, String) + */ + public static final int WEBSOCKET_NORMAL_CLOSURE_CODE = 1000; + + /** + * Defines a websocket normal closure message. + * + * @see WebSocket#close(int, String) + */ + public static final String WEBSOCKET_NORMAL_CLOSURE_MESSAGE = "Normal closure"; + + /** + * Defines the maximum number of reconnection attempts to be made by an {@link AlpacaWebsocket}. + */ + public static int MAX_RECONNECT_ATTEMPTS = 10; + + /** + * Defines the sleep interval {@link Duration} between reconnection attempts made by an {@link AlpacaWebsocket}. + */ + public static Duration RECONNECT_SLEEP_INTERVAL = Duration.ofSeconds(1); + + private static final Logger LOGGER = LoggerFactory.getLogger(AlpacaWebsocket.class); + + protected final OkHttpClient okHttpClient; + protected final HttpUrl websocketURL; + protected final String websocketName; + protected final String keyID; + protected final String secretKey; + protected final String oAuthToken; + protected final boolean useOAuth; + protected L listener; + protected AlpacaWebsocketStateListener alpacaWebsocketStateListener; + protected WebSocket websocket; + protected boolean connected; + protected boolean authenticated; + protected CompletableFuture authenticationMessageFuture; + protected boolean intentionalClose; + protected int reconnectAttempts; + + protected boolean automaticallyReconnect; + + /** + * Instantiates a {@link AlpacaWebsocket}. + * + * @param okHttpClient the {@link OkHttpClient} + * @param websocketURL the websocket {@link HttpUrl} + * @param websocketName the websocket name + * @param keyID the key ID + * @param secretKey the secret key + * @param oAuthToken the OAuth token + */ + public AlpacaWebsocket(OkHttpClient okHttpClient, HttpUrl websocketURL, String websocketName, + String keyID, String secretKey, String oAuthToken) { + checkNotNull(okHttpClient); + checkNotNull(websocketURL); + checkNotNull(websocketName); + + this.okHttpClient = okHttpClient; + this.websocketURL = websocketURL; + this.websocketName = websocketName; + this.keyID = keyID; + this.secretKey = secretKey; + this.oAuthToken = oAuthToken; + useOAuth = oAuthToken != null; + + automaticallyReconnect = true; + } + + @Override + public void connect() { + if (!isConnected()) { + final Request websocketRequest = new Request.Builder() + .url(websocketURL) + .get() + .build(); + websocket = okHttpClient.newWebSocket(websocketRequest, this); + } + } + + @Override + public void disconnect() { + if (websocket != null && isConnected()) { + intentionalClose = true; + websocket.close(WEBSOCKET_NORMAL_CLOSURE_CODE, WEBSOCKET_NORMAL_CLOSURE_MESSAGE); + } else { + cleanupState(); + } + } + + @Override + public boolean isConnected() { + return connected; + } + + @Override + public boolean isAuthenticated() { + return authenticated; + } + + @Override + public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) { + connected = true; + + LOGGER.info("{} websocket opened.", websocketName); + LOGGER.debug("{} websocket response: {}", websocketName, response); + + // Call 'onConnection' or 'onReconnection' async to avoid any potential deadlocking since this is called + // in sync with 'onMessage' in OkHttp's 'WebSocketListener' + commonPool().execute(() -> { + if (reconnectAttempts > 0) { + onReconnection(); + } else { + onConnection(); + } + }); + + if (alpacaWebsocketStateListener != null) { + alpacaWebsocketStateListener.onOpen(response); + } + } + + @Override + public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) { + connected = false; + + if (intentionalClose) { + LOGGER.info("{} websocket closed.", websocketName); + LOGGER.debug("Close code: {}, Reason: {}", code, reason); + cleanupState(); + } else { + LOGGER.error("{} websocket closed unintentionally! Code: {}, Reason: {}", websocketName, code, reason); + handleReconnectionAttempt(); + } + + if (alpacaWebsocketStateListener != null) { + alpacaWebsocketStateListener.onClosed(code, reason); + } + } + + @Override + public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable cause, @Nullable Response response) { + LOGGER.error("{} websocket failure!", websocketName, cause); + + // A websocket failure occurs when either there is a connection failure or when the client throws + // an exception when receiving a message. In either case, OkHttp will close the websocket connection, + // so try to reopen it. + connected = false; + handleReconnectionAttempt(); + + if (alpacaWebsocketStateListener != null) { + alpacaWebsocketStateListener.onFailure(cause); + } + } + + /** + * Attempts to reconnect the disconnected {@link #websocket} asynchronously. + */ + private void handleReconnectionAttempt() { + if (!automaticallyReconnect) { + return; + } + if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { + LOGGER.info("Attempting to reconnect {} websocket in {} seconds... (attempt {} of {})", + websocketName, RECONNECT_SLEEP_INTERVAL.toSeconds(), reconnectAttempts + 1, MAX_RECONNECT_ATTEMPTS); + reconnectAttempts++; + commonPool().execute(() -> { + try { + Thread.sleep(RECONNECT_SLEEP_INTERVAL.toMillis()); + } catch (InterruptedException interruptedException) { + return; + } + connect(); + }); + } else { + LOGGER.error("Exhausted {} reconnection attempts. Not attempting to reconnect.", MAX_RECONNECT_ATTEMPTS); + cleanupState(); + } + } + + /** + * Cleans up this instance's state variables. + */ + protected void cleanupState() { + websocket = null; + connected = false; + authenticated = false; + if (authenticationMessageFuture != null && !authenticationMessageFuture.isDone()) { + authenticationMessageFuture.complete(false); + } + authenticationMessageFuture = null; + intentionalClose = false; + reconnectAttempts = 0; + } + + /** + * Called asynchronously when a websocket connection is made. + */ + protected abstract void onConnection(); + + /** + * Called asynchronously when a websocket reconnection is made after unintentional disconnection. + */ + protected abstract void onReconnection(); + + /** + * Sends an authentication message to authenticate this websocket stream. + */ + protected abstract void sendAuthenticationMessage(); + + @Override + public Future getAuthorizationFuture() { + if (authenticationMessageFuture == null) { + authenticationMessageFuture = new CompletableFuture<>(); + } + return authenticationMessageFuture; + } + + /** + * Calls the {@link AlpacaWebsocketMessageListener}. + * + * @param messageType the message type + * @param message the message + */ + protected void callListener(T messageType, M message) { + if (listener != null) { + try { + listener.onMessage(messageType, message); + } catch (Exception exception) { + LOGGER.error("{} listener threw exception!", websocketName, exception); + } + } + } + + @Override + public void setListener(L listener) { + this.listener = listener; + } + + public AlpacaWebsocketStateListener getWebsocketStateListener() { + return alpacaWebsocketStateListener; + } + + public void setAlpacaWebsocketStateListener(AlpacaWebsocketStateListener alpacaWebsocketStateListener) { + this.alpacaWebsocketStateListener = alpacaWebsocketStateListener; + } + + public boolean doesAutomaticallyReconnect() { + return automaticallyReconnect; + } + + public void setAutomaticallyReconnect(boolean automaticallyReconnect) { + this.automaticallyReconnect = automaticallyReconnect; + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketInterface.java b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketInterface.java new file mode 100644 index 00000000..033475a2 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketInterface.java @@ -0,0 +1,82 @@ +package net.jacobpeterson.alpaca.websocket; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * {@link AlpacaWebsocketInterface} defines an interface for Alpaca websockets. + * + * @param the {@link AlpacaWebsocketMessageListener} type parameter + */ +public interface AlpacaWebsocketInterface> { + + /** + * Connects this Websocket. + */ + void connect(); + + /** + * Disconnects this Websocket. + */ + void disconnect(); + + /** + * Returns true if this websocket is connected, false otherwise. + * + * @return a boolean + */ + boolean isConnected(); + + /** + * Returns true if this websocket is authenticated, false otherwise. + * + * @return a boolean + */ + boolean isAuthenticated(); + + /** + * Returns true if {@link #isConnected()} and {@link #isAuthenticated()}, false + * otherwise. + * + * @return a boolean + */ + default boolean isValid() { + return isConnected() && isAuthenticated(); + } + + /** + * Gets a {@link Boolean} {@link Future} that completes when an authentication message that is received after a new + * websocket connection indicates successful authentication. + *
+ * Note that if this {@link AlpacaWebsocketInterface} is already authorized, the returned {@link Future} will likely + * never complete. + * + * @return a {@link Boolean} {@link Future} + */ + Future getAuthorizationFuture(); + + /** + * Waits for {@link #getAuthorizationFuture()} to complete and returns its value, except when timeout + * time has elapsed, then this will return false. + * + * @param timeout the timeout time + * @param unit the timeout {@link TimeUnit} + * + * @return a boolean + */ + default boolean waitForAuthorization(long timeout, TimeUnit unit) { + try { + return getAuthorizationFuture().get(timeout, unit); + } catch (InterruptedException | ExecutionException | TimeoutException ignored) {} + return false; + } + + /** + * Sets the {@link AlpacaWebsocketMessageListener} for this {@link AlpacaWebsocketInterface}. + * + * @param listener the {@link AlpacaWebsocketMessageListener} + */ + void setListener(L listener); +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketMessageListener.java b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketMessageListener.java new file mode 100644 index 00000000..da55535c --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketMessageListener.java @@ -0,0 +1,20 @@ +package net.jacobpeterson.alpaca.websocket; + +/** + * {@link AlpacaWebsocketMessageListener} defines a listener interface for messages from an {@link AlpacaWebsocket} + * instances. + * + * @param the 'message type' type parameter + * @param the 'message' type parameter + */ +@FunctionalInterface +public interface AlpacaWebsocketMessageListener { + + /** + * Called when a message is received. + * + * @param messageType the message type + * @param message the message + */ + void onMessage(T messageType, M message); +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketStateListener.java b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketStateListener.java new file mode 100644 index 00000000..2667c2ca --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketStateListener.java @@ -0,0 +1,33 @@ +package net.jacobpeterson.alpaca.websocket; + +import okhttp3.Response; +import okhttp3.WebSocket; + +/** + * {@link AlpacaWebsocketStateListener} is an interface to listen for various state changes in an {@link WebSocket} + * instance. + */ +public interface AlpacaWebsocketStateListener { + + /** + * Called when the {@link WebSocket} is connected. + * + * @param response the HTTP websocket upgrade {@link Response} + */ + void onOpen(Response response); + + /** + * Called when the {@link WebSocket} is disconnected. + * + * @param code the code + * @param reason the reason + */ + void onClosed(int code, String reason); + + /** + * Called when a {@link WebSocket} error has occurred. + * + * @param cause the cause {@link Throwable} + */ + void onFailure(Throwable cause); +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesListener.java b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesListener.java new file mode 100644 index 00000000..c96baee5 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesListener.java @@ -0,0 +1,11 @@ +package net.jacobpeterson.alpaca.websocket.trades; + +import net.jacobpeterson.alpaca.model.websocket.trades.model.TradesStreamMessage; +import net.jacobpeterson.alpaca.model.websocket.trades.model.TradesStreamMessageType; +import net.jacobpeterson.alpaca.websocket.AlpacaWebsocketMessageListener; + +/** + * {@link TradesListener} defines a listener interface for {@link TradesWebsocket} messages. + */ +public interface TradesListener extends AlpacaWebsocketMessageListener { +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesListenerAdapter.java b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesListenerAdapter.java new file mode 100644 index 00000000..a6dd93ec --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesListenerAdapter.java @@ -0,0 +1,19 @@ +package net.jacobpeterson.alpaca.websocket.trades; + +import net.jacobpeterson.alpaca.model.websocket.trades.model.TradesStreamMessage; +import net.jacobpeterson.alpaca.model.websocket.trades.model.TradesStreamMessageType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link TradesListenerAdapter} is an adapter class for {@link TradesListener}. + */ +public class TradesListenerAdapter implements TradesListener { + + private static final Logger LOGGER = LoggerFactory.getLogger(TradesListenerAdapter.class); + + @Override + public void onMessage(TradesStreamMessageType messageType, TradesStreamMessage message) { + LOGGER.info("{} message received: {}", messageType, message); + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesWebsocket.java new file mode 100644 index 00000000..a6fc8896 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesWebsocket.java @@ -0,0 +1,219 @@ +package net.jacobpeterson.alpaca.websocket.trades; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import net.jacobpeterson.alpaca.model.util.apitype.TraderAPIEndpointType; +import net.jacobpeterson.alpaca.model.websocket.trades.model.TradesStreamMessage; +import net.jacobpeterson.alpaca.model.websocket.trades.model.TradesStreamMessageType; +import net.jacobpeterson.alpaca.model.websocket.trades.model.authorization.AuthorizationData; +import net.jacobpeterson.alpaca.model.websocket.trades.model.authorization.AuthorizationMessage; +import net.jacobpeterson.alpaca.model.websocket.trades.model.listening.ListeningMessage; +import net.jacobpeterson.alpaca.model.websocket.trades.model.tradeupdate.TradeUpdateMessage; +import net.jacobpeterson.alpaca.websocket.AlpacaWebsocket; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.WebSocket; +import okio.ByteString; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Predicates.not; +import static com.google.common.collect.Iterables.toArray; +import static com.google.gson.JsonParser.parseString; +import static java.lang.String.format; +import static java.util.Collections.singletonList; +import static java.util.concurrent.TimeUnit.SECONDS; +import static net.jacobpeterson.alpaca.model.websocket.trades.model.TradesStreamMessageType.TRADE_UPDATES; +import static net.jacobpeterson.alpaca.openapi.trader.JSON.getGson; + +/** + * {@link TradesWebsocket} is an {@link AlpacaWebsocket} implementation and provides the + * {@link TradesWebsocketInterface} interface for + * Trades Streaming. + */ +public class TradesWebsocket extends AlpacaWebsocket + implements TradesWebsocketInterface { + + private static final Logger LOGGER = LoggerFactory.getLogger(TradesWebsocket.class); + private static final String STREAM_ELEMENT_KEY = "stream"; + private static final List SUBSCRIBABLE_STREAMING_MESSAGE_TYPES = + singletonList(TRADE_UPDATES); + + /** + * Creates a {@link HttpUrl} for {@link TradesWebsocket}. + * + * @param traderAPIEndpointType the {@link TraderAPIEndpointType} + * + * @return a {@link HttpUrl} + */ + @SuppressWarnings("UnnecessaryDefault") + private static HttpUrl createWebsocketURL(TraderAPIEndpointType traderAPIEndpointType) { + return new HttpUrl.Builder() + .scheme("https") + .host((switch (traderAPIEndpointType) { + case LIVE -> "api"; + case PAPER -> "paper-api"; + default -> throw new UnsupportedOperationException(); + }) + ".alpaca.markets") + .addPathSegment("stream") + .build(); + } + + private final Set listenedMessageTypes; + + /** + * Instantiates a new {@link TradesWebsocket}. + * + * @param okHttpClient the {@link OkHttpClient} + * @param traderAPIEndpointType the {@link TraderAPIEndpointType} + * @param keyID the key ID + * @param secretKey the secret key + * @param oAuthToken the OAuth token + */ + public TradesWebsocket(OkHttpClient okHttpClient, TraderAPIEndpointType traderAPIEndpointType, + String keyID, String secretKey, String oAuthToken) { + super(okHttpClient, createWebsocketURL(traderAPIEndpointType), "Trades Stream", keyID, secretKey, oAuthToken); + listenedMessageTypes = new HashSet<>(); + } + + @Override + protected void cleanupState() { + super.cleanupState(); + listenedMessageTypes.clear(); + } + + @Override + protected void onConnection() { + sendAuthenticationMessage(); + } + + @Override + protected void onReconnection() { + sendAuthenticationMessage(); + if (waitForAuthorization(5, SECONDS)) { + subscribe(toArray(listenedMessageTypes, TradesStreamMessageType.class)); + } + } + + @Override + protected void sendAuthenticationMessage() { + // Ensure that the authorization Future exists + getAuthorizationFuture(); + + final JsonObject authObject = new JsonObject(); + authObject.addProperty("action", "authenticate"); + final JsonObject authData = new JsonObject(); + if (useOAuth) { + authData.addProperty("oauth_token", oAuthToken); + } else { + authData.addProperty("key_id", keyID); + authData.addProperty("secret_key", secretKey); + } + authObject.add("data", authData); + + LOGGER.info("{} websocket sending authentication message...", websocketName); + websocket.send(authObject.toString()); + } + + @Override + public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString byteString) { + final String messageString = byteString.utf8(); + LOGGER.trace("Websocket message received: {}", messageString); + + // Parse JSON string and identify 'messageType' + final JsonElement messageElement = parseString(messageString); + checkState(messageElement instanceof JsonObject, "Message must be a JsonObject! Received: %s", messageElement); + final JsonObject messageObject = messageElement.getAsJsonObject(); + final JsonElement streamElement = messageObject.get(STREAM_ELEMENT_KEY); + checkState(streamElement instanceof JsonPrimitive, + "Message must contain %s element! Received: %s", STREAM_ELEMENT_KEY, messageElement); + final TradesStreamMessageType messageType = getGson().fromJson(streamElement, TradesStreamMessageType.class); + checkNotNull(messageType, "TradesStreamMessageType not found in message: %s", messageObject); + + // Deserialize message based on 'messageType' + TradesStreamMessage message; + switch (messageType) { + case AUTHORIZATION: + message = getGson().fromJson(messageObject, AuthorizationMessage.class); + final AuthorizationData authorizationData = ((AuthorizationMessage) message).getData(); + authenticated = authorizationData.getStatus().equalsIgnoreCase("authorized") && + authorizationData.getAction().equalsIgnoreCase("authenticate"); + if (!authenticated) { + throw new RuntimeException(format("%s websocket not authenticated! Received: %s.", + websocketName, message)); + } else { + LOGGER.info("{} websocket authenticated.", websocketName); + } + if (authenticationMessageFuture != null) { + authenticationMessageFuture.complete(authenticated); + } + break; + case LISTENING: + message = getGson().fromJson(messageObject, ListeningMessage.class); + // Remove all 'TradeStreamMessageType's that are no longer listened to and add new ones + final List currentTypes = ((ListeningMessage) message).getData().getStreams(); + currentTypes.stream() + .filter(not(listenedMessageTypes::contains)) + .forEach(listenedMessageTypes::remove); + listenedMessageTypes.addAll(currentTypes); + break; + case TRADE_UPDATES: + message = getGson().fromJson(messageObject, TradeUpdateMessage.class); + break; + default: + throw new UnsupportedOperationException(); + } + + // Call listener + if (listenedMessageTypes.contains(messageType)) { + callListener(messageType, message); + } + } + + @Override + public void subscribe(TradesStreamMessageType... messageTypes) { + if (messageTypes == null || messageTypes.length == 0) { + return; + } + + // Add all non-subscribable 'messageTypes' before connecting or sending websocket message + Arrays.stream(messageTypes) + .filter(not(SUBSCRIBABLE_STREAMING_MESSAGE_TYPES::contains)) + .forEach(listenedMessageTypes::add); + + // Create streams subscription JSON message + final JsonObject requestObject = new JsonObject(); + requestObject.addProperty("action", "listen"); + final JsonArray streamsArray = new JsonArray(); + Arrays.stream(messageTypes) + .filter(SUBSCRIBABLE_STREAMING_MESSAGE_TYPES::contains) + .forEach((type) -> streamsArray.add(type.toString())); + if (streamsArray.isEmpty()) { + return; + } else if (!isConnected()) { + throw new IllegalStateException("This websocket must be connected before subscribing to streams!"); + } + final JsonObject dataObject = new JsonObject(); + dataObject.add("streams", streamsArray); + requestObject.add("data", dataObject); + + websocket.send(requestObject.toString()); + LOGGER.info("Requested streams: {}.", streamsArray); + } + + @Override + public Collection subscriptions() { + return new HashSet<>(listenedMessageTypes); + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesWebsocketInterface.java b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesWebsocketInterface.java new file mode 100644 index 00000000..b866e37e --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesWebsocketInterface.java @@ -0,0 +1,26 @@ +package net.jacobpeterson.alpaca.websocket.trades; + +import net.jacobpeterson.alpaca.model.websocket.trades.model.TradesStreamMessageType; +import net.jacobpeterson.alpaca.websocket.AlpacaWebsocketInterface; + +import java.util.Collection; + +/** + * {@link TradesWebsocketInterface} is an {@link AlpacaWebsocketInterface} for a {@link TradesWebsocket}. + */ +public interface TradesWebsocketInterface extends AlpacaWebsocketInterface { + + /** + * Sets the {@link TradesStreamMessageType}s for this stream. + * + * @param messageTypes the {@link TradesStreamMessageType}s + */ + void subscribe(TradesStreamMessageType... messageTypes); + + /** + * Gets all the currently subscribed {@link TradesStreamMessageType}s. + * + * @return a {@link Collection} of {@link TradesStreamMessageType} + */ + Collection subscriptions(); +} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/authorization/authorization_data.json b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/authorization/authorization_data.json similarity index 69% rename from src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/authorization/authorization_data.json rename to src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/authorization/authorization_data.json index e88df4d5..f05ae9bd 100644 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/authorization/authorization_data.json +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/authorization/authorization_data.json @@ -1,6 +1,5 @@ { "type": "object", - "title": "See Streaming.", "properties": { "status": { "existingJavaType": "java.lang.String", diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/authorization/authorization_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/authorization/authorization_message.json new file mode 100644 index 00000000..82edef4d --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/authorization/authorization_message.json @@ -0,0 +1,12 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.trades.model.TradesStreamMessage" + }, + "properties": { + "data": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.trades.model.authorization.AuthorizationData", + "title": "The {@link net.jacobpeterson.alpaca.model.websocket.trades.model.authorization.AuthorizationData}." + } + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/listening/listening_data.json b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/listening/listening_data.json new file mode 100644 index 00000000..78a68ba5 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/listening/listening_data.json @@ -0,0 +1,9 @@ +{ + "type": "object", + "properties": { + "streams": { + "existingJavaType": "java.util.ArrayList", + "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.websocket.trades.model.TradesStreamMessageType}s." + } + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/listening/listening_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/listening/listening_message.json new file mode 100644 index 00000000..58b7b54f --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/listening/listening_message.json @@ -0,0 +1,12 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.trades.model.TradesStreamMessage" + }, + "properties": { + "data": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.trades.model.listening.ListeningData", + "title": "The {@link net.jacobpeterson.alpaca.model.websocket.trades.model.listening.ListeningData}." + } + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/trades_stream_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/trades_stream_message.json new file mode 100644 index 00000000..fa337195 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/trades_stream_message.json @@ -0,0 +1,9 @@ +{ + "type": "object", + "properties": { + "stream": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.trades.model.TradesStreamMessageType", + "title": "The {@link net.jacobpeterson.alpaca.model.websocket.trades.model.TradesStreamMessageType}." + } + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/trades_stream_message_type.json b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/trades_stream_message_type.json new file mode 100644 index 00000000..98ab6ea6 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/trades_stream_message_type.json @@ -0,0 +1,8 @@ +{ + "type": "string", + "enum": [ + "listening", + "authorization", + "trade_updates" + ] +} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/trade/trade_update.json b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/tradeupdate/trade_update.json similarity index 52% rename from src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/trade/trade_update.json rename to src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/tradeupdate/trade_update.json index c0072275..ab5b6875 100644 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/trade/trade_update.json +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/tradeupdate/trade_update.json @@ -1,24 +1,32 @@ { "type": "object", - "title": "See Streaming.", "properties": { "event": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.streaming.trade.enums.TradeUpdateEvent", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.streaming.trade.enums.TradeUpdateEvent}." + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.trades.model.tradeupdate.TradeUpdateEvent", + "title": "The {@link net.jacobpeterson.alpaca.model.websocket.trades.model.tradeupdate.TradeUpdateEvent}." + }, + "execution_id": { + "existingJavaType": "java.lang.String", + "title": "The execution ID." }, "price": { "existingJavaType": "java.lang.String", "title": "The price." }, - "timestamp": { - "existingJavaType": "java.time.ZonedDateTime", - "title": "The timestamp." - }, "position_qty": { "existingJavaType": "java.lang.String", "javaName": "positionQuantity", "title": "The position quantity." }, + "qty": { + "existingJavaType": "java.lang.String", + "javaName": "quantity", + "title": "The quantity." + }, + "timestamp": { + "existingJavaType": "java.time.OffsetDateTime", + "title": "The timestamp." + }, "order": { "existingJavaType": "net.jacobpeterson.alpaca.openapi.trader.model.Order", "title": "The {@link net.jacobpeterson.alpaca.openapi.trader.model.Order}." diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/trade/enums/trade_update_event.json b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/tradeupdate/trade_update_event.json similarity index 83% rename from src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/trade/enums/trade_update_event.json rename to src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/tradeupdate/trade_update_event.json index e422fe52..1277728e 100644 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/streaming/trade/enums/trade_update_event.json +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/tradeupdate/trade_update_event.json @@ -1,6 +1,5 @@ { "type": "string", - "title": "See Orders.", "enum": [ "new", "fill", @@ -24,13 +23,13 @@ "description": "Sent when an order has been routed to exchanges for execution." }, { - "description": "Sent when your order has been completely filled." + "description": "Sent when an order has been completely filled." }, { "description": "Sent when a number of shares less than the total remaining quantity on your order has been filled." }, { - "description": "Sent when your requested cancelation of an order is processed." + "description": "Sent when your requested cancellation of an order is processed." }, { "description": "Sent when an order has reached the end of its lifespan, as determined by the order’s time in force value." @@ -51,7 +50,7 @@ "description": "Sent when your order has been stopped, and a trade is guaranteed for the order, usually at a stated price or better, but has not yet occurred." }, { - "description": "Sent when the order is awaiting cancelation. Most cancelations will occur without the order entering this state." + "description": "Sent when the order is awaiting cancellation. Most cancellations will occur without the order entering this state." }, { "description": "Sent when the order is awaiting replacement." diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/tradeupdate/trade_update_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/tradeupdate/trade_update_message.json new file mode 100644 index 00000000..6876c589 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/tradeupdate/trade_update_message.json @@ -0,0 +1,12 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.trades.model.TradesStreamMessage" + }, + "properties": { + "data": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.trades.model.tradeupdate.TradeUpdate", + "title": "The {@link net.jacobpeterson.alpaca.model.websocket.trades.model.tradeupdate.TradeUpdate}." + } + } +} From 27e951b2aa2f3c5934c8b2809afb1a07fbfe0b35 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Mon, 4 Mar 2024 21:54:44 -0800 Subject: [PATCH 61/84] WIP update market data websocket POJO JSON schemas --- .../common/historical/bar/bar.json | 41 ----------- .../historical/bar/enums/bar_time_period.json | 28 -------- .../common/historical/quote/quote.json | 21 ------ .../common/historical/trade/trade.json | 21 ------ .../control/subscriptions_message.json | 25 ------- .../realtime/control/success_message.json | 14 ---- .../common/realtime/market_data_message.json | 11 --- .../common/realtime/quote/quote_message.json | 24 ------- .../common/realtime/symbol_message.json | 14 ---- .../common/realtime/trade/trade_message.json | 24 ------- .../crypto/historical/bar/crypto_bar.json | 14 ---- .../historical/bar/crypto_bars_response.json | 14 ---- .../bar/latest_crypto_bars_response.json | 10 --- .../orderbook/crypto_orderbook.json | 21 ------ .../latest_crypto_orderbooks_response.json | 10 --- .../crypto/historical/quote/crypto_quote.json | 19 ------ .../quote/latest_crypto_quotes_response.json | 10 --- .../historical/snapshot/crypto_snapshot.json | 31 --------- .../snapshot/crypto_snapshots_response.json | 10 --- .../crypto/historical/trade/crypto_trade.json | 19 ------ .../trade/crypto_trades_response.json | 14 ---- .../trade/latest_crypto_trades_response.json | 10 --- .../realtime/bar/crypto_bar_message.json | 14 ---- .../realtime/quote/crypto_quote_message.json | 19 ------ .../realtime/trade/crypto_trade_message.json | 19 ------ .../market_data/news/common/image.json | 13 ---- .../market_data/news/common/news_article.json | 39 ----------- .../market_data/news/news_response.json | 12 ---- .../news/realtime/news_message.json | 40 ----------- .../historical/bar/enums/bar_adjustment.json | 24 ------- .../stock/historical/bar/enums/bar_feed.json | 8 --- .../bar/multi_stock_bars_response.json | 14 ---- .../stock/historical/bar/stock_bar.json | 14 ---- .../historical/bar/stock_bars_response.json | 18 ----- .../quote/latest_stock_quote_response.json | 14 ---- .../stock/historical/quote/stock_quote.json | 34 ---------- .../quote/stock_quotes_response.json | 18 ----- .../stock/historical/snapshot/snapshot.json | 26 ------- .../trade/latest_stock_trade_response.json | 14 ---- .../stock/historical/trade/stock_trade.json | 29 -------- .../trade/stock_trades_response.json | 18 ----- .../stock/realtime/bar/stock_bar_message.json | 14 ---- .../realtime/quote/stock_quote_message.json | 39 ----------- .../realtime/trade/stock_trade_message.json | 29 -------- .../model}/control/error_message.json | 3 +- .../model/control/success_message.json | 13 ++++ .../model/control/success_message_type.json | 7 ++ .../marketdata/model/market_data_message.json | 4 ++ .../crypto/CryptoMarketDataWebsocket.java | 27 ++++++++ .../crypto/model/bar/crypto_bar_message.json | 53 +++++++++++++++ .../model/crypto_market_data_message.json | 13 ++++ .../crypto_market_data_message_type.json} | 19 ++++-- .../orderbook/crypto_orderbook_entry.json | 5 +- .../orderbook/crypto_orderbook_message.json | 28 ++++++++ .../model/quote/crypto_quote_message.json | 38 +++++++++++ .../crypto_subscriptions_message.json | 33 +++++++++ .../model/trade/crypto_trade_message.json | 38 +++++++++++ .../model/trade/crypto_trade_taker_side.json} | 1 - .../streams/news/NewsMarketDataWebsocket.java | 15 ++++ .../streams/news/model/news/news_message.json | 38 +++++++++++ .../news/model/news_market_data_message.json | 13 ++++ .../model/news_market_data_message_type.json | 23 +++++++ .../stock/StockMarketDataWebsocket.java | 30 ++++++++ .../stock/model/bar/stock_bar_message.json} | 27 +++++--- ...tock_limit_up_limit_down_band_message.json | 38 +++++++++++ .../model/quote/stock_quote_message.json | 58 ++++++++++++++++ .../model/stock_market_data_message.json | 13 ++++ .../model/stock_market_data_message_type.json | 55 +++++++++++++++ .../stock_subscriptions_message.json | 49 +++++++++++++ .../model/trade/stock_trade_message.json | 48 +++++++++++++ .../stock_trade_cancel_error_action.json | 15 ++++ .../stock_trade_cancel_error_message.json | 48 +++++++++++++ .../stock_trade_correction_message.json | 68 +++++++++++++++++++ .../stock_trading_status_message.json | 43 ++++++++++++ .../websocket/trades/TradesWebsocket.java | 10 ++- 75 files changed, 846 insertions(+), 901 deletions(-) delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/historical/bar/bar.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/historical/bar/enums/bar_time_period.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/historical/quote/quote.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/historical/trade/trade.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/control/subscriptions_message.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/control/success_message.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/market_data_message.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/quote/quote_message.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/symbol_message.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/trade/trade_message.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/bar/crypto_bar.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/bar/crypto_bars_response.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/bar/latest_crypto_bars_response.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/orderbook/crypto_orderbook.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/orderbook/latest_crypto_orderbooks_response.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/quote/crypto_quote.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/quote/latest_crypto_quotes_response.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/snapshot/crypto_snapshot.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/snapshot/crypto_snapshots_response.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/trade/crypto_trade.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/trade/crypto_trades_response.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/trade/latest_crypto_trades_response.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/realtime/bar/crypto_bar_message.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/realtime/quote/crypto_quote_message.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/realtime/trade/crypto_trade_message.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/news/common/image.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/news/common/news_article.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/news/news_response.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/news/realtime/news_message.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/bar/enums/bar_adjustment.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/bar/enums/bar_feed.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/bar/multi_stock_bars_response.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/bar/stock_bar.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/bar/stock_bars_response.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/quote/latest_stock_quote_response.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/quote/stock_quote.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/quote/stock_quotes_response.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/snapshot/snapshot.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/trade/latest_stock_trade_response.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/trade/stock_trade.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/trade/stock_trades_response.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/realtime/bar/stock_bar_message.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/realtime/quote/stock_quote_message.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/realtime/trade/stock_trade_message.json rename src/main/java/net/jacobpeterson/alpaca/{endpoint/market_data/common/realtime => websocket/marketdata/model}/control/error_message.json (57%) create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/model/control/success_message.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/model/control/success_message_type.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/model/market_data_message.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/CryptoMarketDataWebsocket.java create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/bar/crypto_bar_message.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/crypto_market_data_message.json rename src/main/java/net/jacobpeterson/alpaca/{endpoint/market_data/common/realtime/enums/market_data_message_type.json => websocket/marketdata/streams/crypto/model/crypto_market_data_message_type.json} (58%) rename src/main/java/net/jacobpeterson/alpaca/{endpoint/market_data/crypto/historical => websocket/marketdata/streams/crypto/model}/orderbook/crypto_orderbook_entry.json (57%) create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/orderbook/crypto_orderbook_message.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/quote/crypto_quote_message.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/subscriptions/crypto_subscriptions_message.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/trade/crypto_trade_message.json rename src/main/java/net/jacobpeterson/alpaca/{endpoint/market_data/crypto/common/enums/taker_side.json => websocket/marketdata/streams/crypto/model/trade/crypto_trade_taker_side.json} (52%) create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/NewsMarketDataWebsocket.java create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/model/news/news_message.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/model/news_market_data_message.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/model/news_market_data_message_type.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/StockMarketDataWebsocket.java rename src/main/java/net/jacobpeterson/alpaca/{endpoint/market_data/common/realtime/bar/bar_message.json => websocket/marketdata/streams/stock/model/bar/stock_bar_message.json} (51%) create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/limituplimitdownband/stock_limit_up_limit_down_band_message.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/quote/stock_quote_message.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/stock_market_data_message.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/stock_market_data_message_type.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/subscriptions/stock_subscriptions_message.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/trade/stock_trade_message.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/tradecancelerror/stock_trade_cancel_error_action.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/tradecancelerror/stock_trade_cancel_error_message.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/tradecorrection/stock_trade_correction_message.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/tradingstatus/stock_trading_status_message.json diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/historical/bar/bar.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/historical/bar/bar.json deleted file mode 100644 index b2f3b42f..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/historical/bar/bar.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "type": "object", - "title": "See Market Data.", - "properties": { - "t": { - "existingJavaType": "java.time.ZonedDateTime", - "javaName": "timestamp", - "title": "Timestamp with nanosecond precision." - }, - "o": { - "existingJavaType": "java.lang.Double", - "javaName": "open", - "title": "Open price." - }, - "h": { - "existingJavaType": "java.lang.Double", - "javaName": "high", - "title": "High price." - }, - "l": { - "existingJavaType": "java.lang.Double", - "javaName": "low", - "title": "Low price." - }, - "c": { - "existingJavaType": "java.lang.Double", - "javaName": "close", - "title": "Close price." - }, - "n": { - "existingJavaType": "java.lang.Long", - "javaName": "tradeCount", - "title": "Trade count." - }, - "vw": { - "existingJavaType": "java.lang.Double", - "javaName": "vwap", - "title": "VWAP (Volume Weighted Average Price)." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/historical/bar/enums/bar_time_period.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/historical/bar/enums/bar_time_period.json deleted file mode 100644 index 0305b24e..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/historical/bar/enums/bar_time_period.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "type": "string", - "title": "See Market Data.", - "enum": [ - "Min", - "Hour", - "Day", - "Week", - "Month" - ], - "javaEnums": [ - { - "name": "MINUTE" - }, - { - "name": "HOUR" - }, - { - "name": "DAY" - }, - { - "name": "WEEK" - }, - { - "name": "MONTH" - } - ] -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/historical/quote/quote.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/historical/quote/quote.json deleted file mode 100644 index ed75e88f..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/historical/quote/quote.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "type": "object", - "title": "See Market Data.", - "properties": { - "t": { - "existingJavaType": "java.time.ZonedDateTime", - "javaName": "timestamp", - "title": "Timestamp with nanosecond precision." - }, - "ap": { - "existingJavaType": "java.lang.Double", - "javaName": "askPrice", - "title": "Ask price." - }, - "bp": { - "existingJavaType": "java.lang.Double", - "javaName": "bidPrice", - "title": "Bid price." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/historical/trade/trade.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/historical/trade/trade.json deleted file mode 100644 index 1cacafa5..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/historical/trade/trade.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "type": "object", - "title": "See Market Data.", - "properties": { - "t": { - "existingJavaType": "java.time.ZonedDateTime", - "javaName": "timestamp", - "title": "Timestamp with nanosecond precision." - }, - "p": { - "existingJavaType": "java.lang.Double", - "javaName": "price", - "title": "Trade price." - }, - "i": { - "existingJavaType": "java.lang.Long", - "javaName": "tradeID", - "title": "Trade ID." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/control/subscriptions_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/control/subscriptions_message.json deleted file mode 100644 index b44cbb85..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/control/subscriptions_message.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "type": "object", - "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.MarketDataMessage" - }, - "title": "See Market Data.", - "properties": { - "trades": { - "existingJavaType": "java.util.ArrayList", - "title": "The {@link java.util.ArrayList} of subscribed trades." - }, - "quotes": { - "existingJavaType": "java.util.ArrayList", - "title": "The {@link java.util.ArrayList} of subscribed quotes." - }, - "bars": { - "existingJavaType": "java.util.ArrayList", - "title": "The {@link java.util.ArrayList} of subscribed bars." - }, - "news": { - "existingJavaType": "java.util.ArrayList", - "title": "The {@link java.util.ArrayList} of subscribed news." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/control/success_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/control/success_message.json deleted file mode 100644 index 2edd335b..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/control/success_message.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "object", - "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.MarketDataMessage" - }, - "title": "See Market Data.", - "properties": { - "msg": { - "existingJavaType": "java.lang.String", - "javaName": "message", - "title": "The success message." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/market_data_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/market_data_message.json deleted file mode 100644 index d41cd481..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/market_data_message.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "type": "object", - "title": "See Market Data.", - "properties": { - "T": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.enums.MarketDataMessageType", - "javaName": "messageType", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.enums.MarketDataMessageType}." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/quote/quote_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/quote/quote_message.json deleted file mode 100644 index d6bdaabc..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/quote/quote_message.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "type": "object", - "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.SymbolMessage" - }, - "title": "See Market Data.", - "properties": { - "ap": { - "existingJavaType": "java.lang.Double", - "javaName": "askPrice", - "title": "Ask price." - }, - "bp": { - "existingJavaType": "java.lang.Double", - "javaName": "bidPrice", - "title": "Bid price." - }, - "t": { - "existingJavaType": "java.time.ZonedDateTime", - "javaName": "timestamp", - "title": "Timestamp with nanosecond precision." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/symbol_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/symbol_message.json deleted file mode 100644 index a6f40f69..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/symbol_message.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "object", - "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.MarketDataMessage" - }, - "title": "See Market Data.", - "properties": { - "S": { - "existingJavaType": "java.lang.String", - "javaName": "symbol", - "title": "The symbol." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/trade/trade_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/trade/trade_message.json deleted file mode 100644 index ca01450f..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/trade/trade_message.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "type": "object", - "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.SymbolMessage" - }, - "title": "See Market Data.", - "properties": { - "i": { - "existingJavaType": "java.lang.Integer", - "javaName": "tradeID", - "title": "Trade ID." - }, - "p": { - "existingJavaType": "java.lang.Double", - "javaName": "price", - "title": "Trade price." - }, - "t": { - "existingJavaType": "java.time.ZonedDateTime", - "javaName": "timestamp", - "title": "Timestamp with nanosecond precision." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/bar/crypto_bar.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/bar/crypto_bar.json deleted file mode 100644 index 1fbf8b5d..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/bar/crypto_bar.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "object", - "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.historical.bar.Bar" - }, - "title": "See Historical Crypto Data.", - "properties": { - "v": { - "existingJavaType": "java.lang.Double", - "javaName": "volume", - "title": "Volume." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/bar/crypto_bars_response.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/bar/crypto_bars_response.json deleted file mode 100644 index 8461d5cc..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/bar/crypto_bars_response.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "object", - "title": "See Historical Crypto Data.", - "properties": { - "bars": { - "existingJavaType": "java.util.Map>", - "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.bar.CryptoBar}s." - }, - "next_page_token": { - "existingJavaType": "java.lang.String", - "title": "Token that can be used to query the next page." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/bar/latest_crypto_bars_response.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/bar/latest_crypto_bars_response.json deleted file mode 100644 index b502ddc3..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/bar/latest_crypto_bars_response.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "type": "object", - "title": "See Historical Crypto Data.", - "properties": { - "bars": { - "existingJavaType": "java.util.Map", - "title": "The {@link java.util.Map} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.bar.CryptoBar}s." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/orderbook/crypto_orderbook.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/orderbook/crypto_orderbook.json deleted file mode 100644 index 04409010..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/orderbook/crypto_orderbook.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "type": "object", - "title": "See Latest Orderbook.", - "properties": { - "t": { - "existingJavaType": "java.time.ZonedDateTime", - "javaName": "timestamp", - "title": "Timestamp in RFC-3339 format with nanosecond precision." - }, - "b": { - "existingJavaType": "java.util.ArrayList", - "javaName": "bidSide", - "title": "An {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.orderbook.CryptoOrderbookEntry}s for the buy/bid side." - }, - "a": { - "existingJavaType": "java.util.ArrayList", - "javaName": "askSide", - "title": "An {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.orderbook.CryptoOrderbookEntry}s for the ask/sell side." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/orderbook/latest_crypto_orderbooks_response.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/orderbook/latest_crypto_orderbooks_response.json deleted file mode 100644 index 31c605d3..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/orderbook/latest_crypto_orderbooks_response.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "type": "object", - "title": "See Historical Crypto Data.", - "properties": { - "orderbooks": { - "existingJavaType": "java.util.Map", - "title": "The {@link java.util.Map} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.orderbook.CryptoOrderbook}s." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/quote/crypto_quote.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/quote/crypto_quote.json deleted file mode 100644 index 97e50e0b..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/quote/crypto_quote.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "object", - "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.historical.quote.Quote" - }, - "title": "See Historical Crypto Data.", - "properties": { - "as": { - "existingJavaType": "java.lang.Double", - "javaName": "askSize", - "title": "Ask size." - }, - "bs": { - "existingJavaType": "java.lang.Double", - "javaName": "bidSize", - "title": "Bid size." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/quote/latest_crypto_quotes_response.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/quote/latest_crypto_quotes_response.json deleted file mode 100644 index 995ccd02..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/quote/latest_crypto_quotes_response.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "type": "object", - "title": "See Historical Crypto Data.", - "properties": { - "quotes": { - "existingJavaType": "java.util.Map", - "title": "The {@link java.util.Map} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.CryptoQuote}s." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/snapshot/crypto_snapshot.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/snapshot/crypto_snapshot.json deleted file mode 100644 index 9ff6ab0d..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/snapshot/crypto_snapshot.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "type": "object", - "title": "See Snapshots.", - "properties": { - "dailyBar": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.bar.CryptoBar", - "javaName": "dailyBar", - "title": "OHLC aggregate of all the trades in a given interval." - }, - "latestQuote": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.quote.CryptoQuote", - "javaName": "latestQuote", - "title": "The best bid and ask information for a given security." - }, - "latestTrade": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.CryptoTrade", - "javaName": "latestTrade", - "title": "A crypto trade" - }, - "minuteBar": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.bar.CryptoBar", - "javaName": "minuteBar", - "title": "OHLC aggregate of all the trades in a given interval." - }, - "prevDailyBar": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.bar.CryptoBar", - "javaName": "prevDailyBar", - "title": "OHLC aggregate of all the trades in a given interval." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/snapshot/crypto_snapshots_response.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/snapshot/crypto_snapshots_response.json deleted file mode 100644 index 3ec3f3b8..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/snapshot/crypto_snapshots_response.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "type": "object", - "title": "See Historical Crypto Data.", - "properties": { - "snapshots": { - "existingJavaType": "java.util.Map", - "title": "The {@link java.util.Map} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.snapshot.CryptoSnapshot}s." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/trade/crypto_trade.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/trade/crypto_trade.json deleted file mode 100644 index 8e5bf0da..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/trade/crypto_trade.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "object", - "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.historical.trade.Trade" - }, - "title": "See Historical Crypto Data.", - "properties": { - "s": { - "existingJavaType": "java.lang.Double", - "javaName": "size", - "title": "Trade size." - }, - "tks": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.common.enums.TakerSide", - "javaName": "takerSide", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.common.enums.TakerSide} of the trade." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/trade/crypto_trades_response.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/trade/crypto_trades_response.json deleted file mode 100644 index 0a007487..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/trade/crypto_trades_response.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "object", - "title": "See Historical Crypto Data.", - "properties": { - "trades": { - "existingJavaType": "java.util.Map>", - "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.CryptoTrade}s." - }, - "next_page_token": { - "existingJavaType": "java.lang.String", - "title": "Token that can be used to query the next page." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/trade/latest_crypto_trades_response.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/trade/latest_crypto_trades_response.json deleted file mode 100644 index 4e6749c2..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/trade/latest_crypto_trades_response.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "type": "object", - "title": "See Historical Crypto Data.", - "properties": { - "trades": { - "existingJavaType": "java.util.Map", - "title": "The {@link java.util.Map} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.historical.trade.CryptoTrade}s." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/realtime/bar/crypto_bar_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/realtime/bar/crypto_bar_message.json deleted file mode 100644 index 82cfe47f..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/realtime/bar/crypto_bar_message.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "object", - "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.bar.BarMessage" - }, - "title": "See Realtime Crypto Market Data.", - "properties": { - "v": { - "existingJavaType": "java.lang.Double", - "javaName": "volume", - "title": "Volume." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/realtime/quote/crypto_quote_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/realtime/quote/crypto_quote_message.json deleted file mode 100644 index 1eac99c5..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/realtime/quote/crypto_quote_message.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "object", - "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.quote.QuoteMessage" - }, - "title": "See Realtime Crypto Market Data.", - "properties": { - "as": { - "existingJavaType": "java.lang.Double", - "javaName": "askSize", - "title": "Ask size." - }, - "bs": { - "existingJavaType": "java.lang.Double", - "javaName": "bidSize", - "title": "Bid size." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/realtime/trade/crypto_trade_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/realtime/trade/crypto_trade_message.json deleted file mode 100644 index 09ee5a87..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/realtime/trade/crypto_trade_message.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "object", - "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.trade.TradeMessage" - }, - "title": "See Realtime Crypto Market Data.", - "properties": { - "s": { - "existingJavaType": "java.lang.Double", - "javaName": "size", - "title": "Trade size." - }, - "tks": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.common.enums.TakerSide", - "javaName": "takerSide", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.common.enums.TakerSide} of the trade." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/news/common/image.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/news/common/image.json deleted file mode 100644 index 2f3bf607..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/news/common/image.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "type": "object", - "title": "See News.", - "properties": { - "size": { - "existingJavaType": "java.lang.String" - }, - "url": { - "existingJavaType": "java.lang.String" - } - } - } - \ No newline at end of file diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/news/common/news_article.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/news/common/news_article.json deleted file mode 100644 index ee80a6d7..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/news/common/news_article.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "object", - "title": "See News.", - "properties": { - "author": { - "existingJavaType": "java.lang.String" - }, - "content": { - "existingJavaType": "java.lang.String" - }, - "created_at": { - "existingJavaType": "java.time.ZonedDateTime" - }, - "headline": { - "existingJavaType": "java.lang.String" - }, - "id": { - "existingJavaType": "java.lang.Long" - }, - "images": { - "existingJavaType": "java.util.ArrayList" - }, - "source": { - "existingJavaType": "java.lang.String" - }, - "summary": { - "existingJavaType": "java.lang.String" - }, - "symbols": { - "existingJavaType": "java.util.ArrayList" - }, - "updated_at": { - "existingJavaType": "java.time.ZonedDateTime" - }, - "url": { - "existingJavaType": "java.lang.String" - } - } - } diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/news/news_response.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/news/news_response.json deleted file mode 100644 index b25ed7f9..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/news/news_response.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "type": "object", - "title": "See News.", - "properties": { - "news": { - "existingJavaType": "java.util.ArrayList" - }, - "next_page_token": { - "existingJavaType": "java.lang.String" - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/news/realtime/news_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/news/realtime/news_message.json deleted file mode 100644 index bee011fe..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/news/realtime/news_message.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "type": "object", - "title": "See News.", - "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.MarketDataMessage" - }, - "properties": { - "headline": { - "existingJavaType": "java.lang.String" - }, - "created_at": { - "existingJavaType": "java.time.ZonedDateTime" - }, - "updated_at": { - "existingJavaType": "java.time.ZonedDateTime" - }, - "author": { - "existingJavaType": "java.lang.String" - }, - "summary": { - "existingJavaType": "java.lang.String" - }, - "content": { - "existingJavaType": "java.lang.String" - }, - - "url": { - "existingJavaType": "java.lang.String" - }, - "symbols": { - "existingJavaType": "java.util.ArrayList" - }, - "source": { - "existingJavaType": "java.lang.String" - }, - "id": { - "existingJavaType": "java.lang.Long" - } - } - } diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/bar/enums/bar_adjustment.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/bar/enums/bar_adjustment.json deleted file mode 100644 index d42b31b2..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/bar/enums/bar_adjustment.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "type": "string", - "title": "See Historical Stock Data.", - "enum": [ - "raw", - "split", - "dividend", - "all" - ], - "javaEnums": [ - { - "name": "RAW" - }, - { - "name": "SPLIT" - }, - { - "name": "DIVIDEND" - }, - { - "name": "ALL" - } - ] -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/bar/enums/bar_feed.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/bar/enums/bar_feed.json deleted file mode 100644 index 87d8a1a4..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/bar/enums/bar_feed.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "type": "string", - "title": "See Historical Stock Data.", - "enum": [ - "IEX", - "SIP" - ] -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/bar/multi_stock_bars_response.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/bar/multi_stock_bars_response.json deleted file mode 100644 index 65028159..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/bar/multi_stock_bars_response.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "object", - "title": "See Historical Stock Data.", - "properties": { - "bars": { - "existingJavaType": "java.util.HashMap>", - "title": "The {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.StockBar}s of {@link java.lang.String} symbols {@link java.util.HashMap}." - }, - "next_page_token": { - "existingJavaType": "java.lang.String", - "title": "Token that can be used to query the next page." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/bar/stock_bar.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/bar/stock_bar.json deleted file mode 100644 index ae851453..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/bar/stock_bar.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "object", - "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.historical.bar.Bar" - }, - "title": "See Historical Stock Data.", - "properties": { - "v": { - "existingJavaType": "java.lang.Long", - "javaName": "volume", - "title": "Volume." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/bar/stock_bars_response.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/bar/stock_bars_response.json deleted file mode 100644 index 5add43d2..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/bar/stock_bars_response.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "type": "object", - "title": "See Historical Stock Data.", - "properties": { - "bars": { - "existingJavaType": "java.util.ArrayList", - "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.StockBar}s. This may be null if no bars exist." - }, - "symbol": { - "existingJavaType": "java.lang.String", - "title": "Symbol that was queried." - }, - "next_page_token": { - "existingJavaType": "java.lang.String", - "title": "Token that can be used to query the next page." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/quote/latest_stock_quote_response.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/quote/latest_stock_quote_response.json deleted file mode 100644 index 4a9a3992..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/quote/latest_stock_quote_response.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "object", - "title": "See Historical Stock Data.", - "properties": { - "symbol": { - "existingJavaType": "java.lang.String", - "title": "Symbol that was queried." - }, - "quote": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.StockQuote", - "title": "The latest {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.StockQuote}." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/quote/stock_quote.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/quote/stock_quote.json deleted file mode 100644 index 164f093f..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/quote/stock_quote.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "type": "object", - "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.historical.quote.Quote" - }, - "title": "See Historical Stock Data.", - "properties": { - "ax": { - "existingJavaType": "java.lang.String", - "javaName": "askExchange", - "title": "Ask exchange." - }, - "as": { - "existingJavaType": "java.lang.Integer", - "javaName": "askSize", - "title": "Ask size." - }, - "bx": { - "existingJavaType": "java.lang.String", - "javaName": "bidExchange", - "title": "Bid exchange." - }, - "bs": { - "existingJavaType": "java.lang.Integer", - "javaName": "bidSize", - "title": "Bid size." - }, - "c": { - "existingJavaType": "java.util.ArrayList", - "javaName": "conditions", - "title": "The {@link java.util.ArrayList} of quote conditions." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/quote/stock_quotes_response.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/quote/stock_quotes_response.json deleted file mode 100644 index 418dcf6b..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/quote/stock_quotes_response.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "type": "object", - "title": "See Historical Stock Data.", - "properties": { - "quotes": { - "existingJavaType": "java.util.ArrayList", - "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.StockQuote}s." - }, - "symbol": { - "existingJavaType": "java.lang.String", - "title": "Symbol that was queried." - }, - "next_page_token": { - "existingJavaType": "java.lang.String", - "title": "Token that can be used to query the next page." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/snapshot/snapshot.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/snapshot/snapshot.json deleted file mode 100644 index 9617b3de..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/snapshot/snapshot.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "type": "object", - "title": "See Historical Stock Data.", - "properties": { - "latestTrade": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.StockTrade", - "title": "The latest {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.StockTrade}." - }, - "latestQuote": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.StockQuote", - "title": "The latest {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.quote.StockQuote}." - }, - "minuteBar": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.StockBar", - "title": "The minute {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.StockBar}." - }, - "dailyBar": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.StockBar", - "title": "The daily {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.StockBar}." - }, - "prevDailyBar": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.StockBar", - "title": "The previous daily {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.bar.StockBar}." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/trade/latest_stock_trade_response.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/trade/latest_stock_trade_response.json deleted file mode 100644 index b4765b53..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/trade/latest_stock_trade_response.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "object", - "title": "See Historical Stock Data.", - "properties": { - "symbol": { - "existingJavaType": "java.lang.String", - "title": "Symbol that was queried." - }, - "trade": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.StockTrade", - "title": "The latest {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.StockTrade}." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/trade/stock_trade.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/trade/stock_trade.json deleted file mode 100644 index ea3e96cf..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/trade/stock_trade.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "type": "object", - "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.historical.trade.Trade" - }, - "title": "See Historical Stock Data.", - "properties": { - "x": { - "existingJavaType": "java.lang.String", - "javaName": "exchange", - "title": "Exchange where the trade happened." - }, - "s": { - "existingJavaType": "java.lang.Integer", - "javaName": "size", - "title": "Trade size." - }, - "c": { - "existingJavaType": "java.util.ArrayList", - "javaName": "conditions", - "title": "The {@link java.util.ArrayList} of trade conditions." - }, - "z": { - "existingJavaType": "java.lang.String", - "javaName": "tape", - "title": "Tape." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/trade/stock_trades_response.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/trade/stock_trades_response.json deleted file mode 100644 index 9b08b1da..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/historical/trade/stock_trades_response.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "type": "object", - "title": "See Historical Stock Data.", - "properties": { - "trades": { - "existingJavaType": "java.util.ArrayList", - "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.historical.trade.StockTrade}s." - }, - "symbol": { - "existingJavaType": "java.lang.String", - "title": "Symbol that was queried." - }, - "next_page_token": { - "existingJavaType": "java.lang.String", - "title": "Token that can be used to query the next page." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/realtime/bar/stock_bar_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/realtime/bar/stock_bar_message.json deleted file mode 100644 index 6bfbf62a..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/realtime/bar/stock_bar_message.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "object", - "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.bar.BarMessage" - }, - "title": "See Realtime Stock Market Data.", - "properties": { - "v": { - "existingJavaType": "java.lang.Long", - "javaName": "volume", - "title": "Volume." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/realtime/quote/stock_quote_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/realtime/quote/stock_quote_message.json deleted file mode 100644 index 2633ccde..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/realtime/quote/stock_quote_message.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "object", - "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.quote.QuoteMessage" - }, - "title": "See Realtime Stock Market Data.", - "properties": { - "ax": { - "existingJavaType": "java.lang.String", - "javaName": "askExchange", - "title": "Ask exchange code." - }, - "as": { - "existingJavaType": "java.lang.Integer", - "javaName": "askSize", - "title": "Ask size." - }, - "bx": { - "existingJavaType": "java.lang.String", - "javaName": "bidExchange", - "title": "Bid exchange code." - }, - "bs": { - "existingJavaType": "java.lang.Integer", - "javaName": "bidSize", - "title": "Bid size." - }, - "c": { - "existingJavaType": "java.util.ArrayList", - "javaName": "conditions", - "title": "The {@link java.util.ArrayList} of quote conditions." - }, - "z": { - "existingJavaType": "java.lang.String", - "javaName": "tape", - "title": "Tape." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/realtime/trade/stock_trade_message.json b/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/realtime/trade/stock_trade_message.json deleted file mode 100644 index c3b0bc51..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/stock/realtime/trade/stock_trade_message.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "type": "object", - "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.trade.TradeMessage" - }, - "title": "See Realtime Stock Market Data.", - "properties": { - "x": { - "existingJavaType": "java.lang.String", - "javaName": "exchange", - "title": "Exchange code where the trade occurred." - }, - "s": { - "existingJavaType": "java.lang.Integer", - "javaName": "size", - "title": "Trade size." - }, - "c": { - "existingJavaType": "java.util.ArrayList", - "javaName": "conditions", - "title": "The {@link java.util.ArrayList} of trade conditions." - }, - "z": { - "existingJavaType": "java.lang.String", - "javaName": "tape", - "title": "Tape." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/control/error_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/model/control/error_message.json similarity index 57% rename from src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/control/error_message.json rename to src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/model/control/error_message.json index 39b9e7f2..655d6a03 100644 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/control/error_message.json +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/model/control/error_message.json @@ -1,9 +1,8 @@ { "type": "object", "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.MarketDataMessage" + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.model.MarketDataMessage" }, - "title": "See Market Data.", "properties": { "code": { "existingJavaType": "java.lang.Integer", diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/model/control/success_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/model/control/success_message.json new file mode 100644 index 00000000..e471b400 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/model/control/success_message.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.model.MarketDataMessage" + }, + "properties": { + "msg": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.model.control.SuccessMessageType", + "javaName": "messageType", + "title": "The success message type." + } + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/model/control/success_message_type.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/model/control/success_message_type.json new file mode 100644 index 00000000..61c6c983 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/model/control/success_message_type.json @@ -0,0 +1,7 @@ +{ + "type": "string", + "enum": [ + "success", + "authenticated" + ] +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/model/market_data_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/model/market_data_message.json new file mode 100644 index 00000000..e1a8346a --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/model/market_data_message.json @@ -0,0 +1,4 @@ +{ + "type": "object", + "properties": {} +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/CryptoMarketDataWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/CryptoMarketDataWebsocket.java new file mode 100644 index 00000000..8eed2ce2 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/CryptoMarketDataWebsocket.java @@ -0,0 +1,27 @@ +package net.jacobpeterson.alpaca.websocket.marketdata.streams.crypto; + +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.realtime.bar.CryptoBarMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.realtime.quote.CryptoQuoteMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.realtime.trade.CryptoTradeMessage; +import net.jacobpeterson.alpaca.websocket.marketdata.MarketDataWebsocket; +import okhttp3.OkHttpClient; + +/** + * {@link CryptoMarketDataWebsocket} is a {@link MarketDataWebsocket} for + * Realtime + * Crypto Market Data + */ +public class CryptoMarketDataWebsocket extends MarketDataWebsocket { + + /** + * Instantiates a new {@link CryptoMarketDataWebsocket}. + * + * @param okHttpClient the {@link OkHttpClient} + * @param keyID the key ID + * @param secretKey the secret key + */ + public CryptoMarketDataWebsocket(OkHttpClient okHttpClient, String keyID, String secretKey) { + super(okHttpClient, "v1beta3/crypto/us", "Crypto", keyID, secretKey, CryptoTradeMessage.class, + CryptoQuoteMessage.class, CryptoBarMessage.class); + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/bar/crypto_bar_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/bar/crypto_bar_message.json new file mode 100644 index 00000000..f24e3e99 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/bar/crypto_bar_message.json @@ -0,0 +1,53 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.CryptoMarketDataMessage" + }, + "properties": { + "S": { + "existingJavaType": "java.lang.String", + "javaName": "symbol", + "title": "The symbol." + }, + "o": { + "existingJavaType": "java.lang.Double", + "javaName": "open", + "title": "The open price." + }, + "h": { + "existingJavaType": "java.lang.Double", + "javaName": "high", + "title": "The high price." + }, + "l": { + "existingJavaType": "java.lang.Double", + "javaName": "low", + "title": "The low price." + }, + "c": { + "existingJavaType": "java.lang.Double", + "javaName": "close", + "title": "The close price." + }, + "t": { + "existingJavaType": "java.time.OffsetDateTime", + "javaName": "timestamp", + "title": "The timestamp." + }, + "v": { + "existingJavaType": "java.lang.Long", + "javaName": "volume", + "title": "The volume." + }, + "n": { + "existingJavaType": "java.lang.Long", + "javaName": "tradeCount", + "title": "The trade count." + }, + "vw": { + "existingJavaType": "java.lang.Double", + "javaName": "vwap", + "title": "The VWAP (Volume Weighted Average Price)." + } + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/crypto_market_data_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/crypto_market_data_message.json new file mode 100644 index 00000000..0ced4e8f --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/crypto_market_data_message.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.model.MarketDataMessage" + }, + "properties": { + "T": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.CryptoMarketDataMessageType", + "javaName": "messageType", + "title": "The {@link net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.CryptoMarketDataMessageType}." + } + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/enums/market_data_message_type.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/crypto_market_data_message_type.json similarity index 58% rename from src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/enums/market_data_message_type.json rename to src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/crypto_market_data_message_type.json index 6999050d..a84d1728 100644 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/enums/market_data_message_type.json +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/crypto_market_data_message_type.json @@ -1,6 +1,5 @@ { "type": "string", - "title": "See Market Data.", "enum": [ "success", "error", @@ -8,7 +7,9 @@ "t", "q", "b", - "n" + "d", + "u", + "o" ], "javaEnums": [ { @@ -21,16 +22,22 @@ "name": "SUBSCRIPTION" }, { - "name": "TRADE" + "name": "TRADES" }, { - "name": "QUOTE" + "name": "QUOTES" }, { - "name": "BAR" + "name": "MINUTE_BARS" }, { - "name": "NEWS" + "name": "DAILY_BARS" + }, + { + "name": "UPDATED_BARS" + }, + { + "name": "ORDER_BOOKS" } ] } diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/orderbook/crypto_orderbook_entry.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/orderbook/crypto_orderbook_entry.json similarity index 57% rename from src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/orderbook/crypto_orderbook_entry.json rename to src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/orderbook/crypto_orderbook_entry.json index 5387ecc7..51ce64b5 100644 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/historical/orderbook/crypto_orderbook_entry.json +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/orderbook/crypto_orderbook_entry.json @@ -1,16 +1,15 @@ { "type": "object", - "title": "See Latest Orderbook.", "properties": { "p": { "existingJavaType": "java.lang.Double", "javaName": "price", - "title": "Price." + "title": "The price." }, "s": { "existingJavaType": "java.lang.Double", "javaName": "size", - "title": "Size." + "title": "The size." } } } diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/orderbook/crypto_orderbook_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/orderbook/crypto_orderbook_message.json new file mode 100644 index 00000000..80aa0c54 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/orderbook/crypto_orderbook_message.json @@ -0,0 +1,28 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.CryptoMarketDataMessage" + }, + "properties": { + "S": { + "existingJavaType": "java.lang.String", + "javaName": "symbol", + "title": "The symbol." + }, + "a": { + "existingJavaType": "java.util.ArrayList", + "javaName": "asks", + "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.orderbook.CryptoOrderBookEntry} asks." + }, + "b": { + "existingJavaType": "java.util.ArrayList", + "javaName": "bids", + "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.orderbook.CryptoOrderBookEntry} bids." + }, + "t": { + "existingJavaType": "java.time.OffsetDateTime", + "javaName": "timestamp", + "title": "The timestamp with nanosecond precision." + } + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/quote/crypto_quote_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/quote/crypto_quote_message.json new file mode 100644 index 00000000..d314de7a --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/quote/crypto_quote_message.json @@ -0,0 +1,38 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.CryptoMarketDataMessage" + }, + "properties": { + "S": { + "existingJavaType": "java.lang.String", + "javaName": "symbol", + "title": "The symbol." + }, + "ap": { + "existingJavaType": "java.lang.Double", + "javaName": "askPrice", + "title": "The ask price." + }, + "as": { + "existingJavaType": "java.lang.Double", + "javaName": "askSize", + "title": "The ask size." + }, + "bp": { + "existingJavaType": "java.lang.Double", + "javaName": "bidPrice", + "title": "The bid price." + }, + "bs": { + "existingJavaType": "java.lang.Double", + "javaName": "bidSize", + "title": "The bid size." + }, + "t": { + "existingJavaType": "java.time.OffsetDateTime", + "javaName": "timestamp", + "title": "The timestamp with nanosecond precision." + } + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/subscriptions/crypto_subscriptions_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/subscriptions/crypto_subscriptions_message.json new file mode 100644 index 00000000..19b1f191 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/subscriptions/crypto_subscriptions_message.json @@ -0,0 +1,33 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.CryptoMarketDataMessage" + }, + "properties": { + "trades": { + "existingJavaType": "java.util.ArrayList", + "title": "The {@link java.util.ArrayList} of symbols subscribed to trades." + }, + "quotes": { + "existingJavaType": "java.util.ArrayList", + "title": "The {@link java.util.ArrayList} of symbols subscribed to quotes." + }, + "bars": { + "existingJavaType": "java.util.ArrayList", + "javaName": "minuteBars", + "title": "The {@link java.util.ArrayList} of symbols subscribed to minute bars." + }, + "dailyBars": { + "existingJavaType": "java.util.ArrayList", + "title": "The {@link java.util.ArrayList} of symbols subscribed to daily bars." + }, + "updatedBars": { + "existingJavaType": "java.util.ArrayList", + "title": "The {@link java.util.ArrayList} of symbols subscribed to updated bars." + }, + "orderbooks": { + "existingJavaType": "java.util.ArrayList", + "title": "The {@link java.util.ArrayList} of symbols subscribed to orderbooks." + } + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/trade/crypto_trade_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/trade/crypto_trade_message.json new file mode 100644 index 00000000..13118dd7 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/trade/crypto_trade_message.json @@ -0,0 +1,38 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.CryptoMarketDataMessage" + }, + "properties": { + "S": { + "existingJavaType": "java.lang.String", + "javaName": "symbol", + "title": "The symbol." + }, + "i": { + "existingJavaType": "java.lang.Long", + "javaName": "tradeID", + "title": "The trade ID." + }, + "p": { + "existingJavaType": "java.lang.Double", + "javaName": "price", + "title": "The trade price." + }, + "s": { + "existingJavaType": "java.lang.Double", + "javaName": "size", + "title": "The trade size." + }, + "t": { + "existingJavaType": "java.time.OffsetDateTime", + "javaName": "timestamp", + "title": "The timestamp with nanosecond precision." + }, + "tks": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.trade.CryptoTradeTakerSide", + "javaName": "takerSide", + "title": "The taker side." + } + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/common/enums/taker_side.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/trade/crypto_trade_taker_side.json similarity index 52% rename from src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/common/enums/taker_side.json rename to src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/trade/crypto_trade_taker_side.json index 8c8e4adf..d793fbfb 100644 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/crypto/common/enums/taker_side.json +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/trade/crypto_trade_taker_side.json @@ -1,6 +1,5 @@ { "type": "string", - "title": "See Crypto Data.", "enum": [ "B", "S" diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/NewsMarketDataWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/NewsMarketDataWebsocket.java new file mode 100644 index 00000000..8e92fa69 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/NewsMarketDataWebsocket.java @@ -0,0 +1,15 @@ +package net.jacobpeterson.alpaca.websocket.marketdata.streams.news; + +import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.bar.BarMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.quote.QuoteMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.trade.TradeMessage; +import net.jacobpeterson.alpaca.websocket.marketdata.MarketDataWebsocket; +import okhttp3.OkHttpClient; + +public class NewsMarketDataWebsocket extends MarketDataWebsocket { + public NewsMarketDataWebsocket(OkHttpClient okHttpClient, + String keyID, String secretKey) { + super(okHttpClient, "v1beta1/news", "News", keyID, secretKey, TradeMessage.class, + QuoteMessage.class, BarMessage.class); + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/model/news/news_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/model/news/news_message.json new file mode 100644 index 00000000..047c0790 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/model/news/news_message.json @@ -0,0 +1,38 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.streams.news.model.NewsMarketDataMessage" + }, + "properties": { + "id": { + "existingJavaType": "java.lang.Long" + }, + "source": { + "existingJavaType": "java.lang.String" + }, + "headline": { + "existingJavaType": "java.lang.String" + }, + "author": { + "existingJavaType": "java.lang.String" + }, + "summary": { + "existingJavaType": "java.lang.String" + }, + "content": { + "existingJavaType": "java.lang.String" + }, + "url": { + "existingJavaType": "java.lang.String" + }, + "symbols": { + "existingJavaType": "java.util.ArrayList" + }, + "created_at": { + "existingJavaType": "java.time.OffsetDateTime" + }, + "updated_at": { + "existingJavaType": "java.time.OffsetDateTime" + } + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/model/news_market_data_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/model/news_market_data_message.json new file mode 100644 index 00000000..1f967c5b --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/model/news_market_data_message.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.model.MarketDataMessage" + }, + "properties": { + "T": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.streams.news.model.NewsMarketDataMessageType", + "javaName": "messageType", + "title": "The {@link net.jacobpeterson.alpaca.model.websocket.marketdata.streams.news.model.NewsMarketDataMessageType}." + } + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/model/news_market_data_message_type.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/model/news_market_data_message_type.json new file mode 100644 index 00000000..4ddd6017 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/model/news_market_data_message_type.json @@ -0,0 +1,23 @@ +{ + "type": "string", + "enum": [ + "success", + "error", + "subscription", + "n" + ], + "javaEnums": [ + { + "name": "SUCCESS" + }, + { + "name": "ERROR" + }, + { + "name": "SUBSCRIPTION" + }, + { + "name": "NEWS" + } + ] +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/StockMarketDataWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/StockMarketDataWebsocket.java new file mode 100644 index 00000000..87bfe6c9 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/StockMarketDataWebsocket.java @@ -0,0 +1,30 @@ +package net.jacobpeterson.alpaca.websocket.marketdata.streams.stock; + +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.bar.StockBarMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.quote.StockQuoteMessage; +import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.trade.StockTradeMessage; +import net.jacobpeterson.alpaca.model.properties.DataAPIType; +import net.jacobpeterson.alpaca.websocket.marketdata.MarketDataWebsocket; +import okhttp3.OkHttpClient; + +/** + * {@link StockMarketDataWebsocket} is a {@link MarketDataWebsocket} for + * Realtime + * Stock Market Data + */ +public class StockMarketDataWebsocket extends MarketDataWebsocket { + + /** + * Instantiates a new {@link StockMarketDataWebsocket}. + * + * @param okHttpClient the {@link OkHttpClient} + * @param dataAPIType the {@link DataAPIType} + * @param keyID the key ID + * @param secretKey the secret key + */ + public StockMarketDataWebsocket(OkHttpClient okHttpClient, DataAPIType dataAPIType, + String keyID, String secretKey) { + super(okHttpClient, "v2/" + dataAPIType.toString(), "Stock", keyID, secretKey, StockTradeMessage.class, + StockQuoteMessage.class, StockBarMessage.class); + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/bar/bar_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/bar/stock_bar_message.json similarity index 51% rename from src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/bar/bar_message.json rename to src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/bar/stock_bar_message.json index 9df2a564..95c6c0d3 100644 --- a/src/main/java/net/jacobpeterson/alpaca/endpoint/market_data/common/realtime/bar/bar_message.json +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/bar/stock_bar_message.json @@ -1,44 +1,53 @@ { "type": "object", "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.SymbolMessage" + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.StockMarketDataMessage" }, - "title": "See Market Data.", "properties": { + "S": { + "existingJavaType": "java.lang.String", + "javaName": "symbol", + "title": "The symbol." + }, "o": { "existingJavaType": "java.lang.Double", "javaName": "open", - "title": "Open price." + "title": "The open price." }, "h": { "existingJavaType": "java.lang.Double", "javaName": "high", - "title": "High price." + "title": "The high price." }, "l": { "existingJavaType": "java.lang.Double", "javaName": "low", - "title": "Low price." + "title": "The low price." }, "c": { "existingJavaType": "java.lang.Double", "javaName": "close", - "title": "Close price." + "title": "The close price." }, "t": { - "existingJavaType": "java.time.ZonedDateTime", + "existingJavaType": "java.time.OffsetDateTime", "javaName": "timestamp", "title": "The timestamp." }, + "v": { + "existingJavaType": "java.lang.Long", + "javaName": "volume", + "title": "The volume." + }, "n": { "existingJavaType": "java.lang.Long", "javaName": "tradeCount", - "title": "Trade count." + "title": "The trade count." }, "vw": { "existingJavaType": "java.lang.Double", "javaName": "vwap", - "title": "VWAP (Volume Weighted Average Price)." + "title": "The VWAP (Volume Weighted Average Price)." } } } diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/limituplimitdownband/stock_limit_up_limit_down_band_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/limituplimitdownband/stock_limit_up_limit_down_band_message.json new file mode 100644 index 00000000..db9238cf --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/limituplimitdownband/stock_limit_up_limit_down_band_message.json @@ -0,0 +1,38 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.StockMarketDataMessage" + }, + "properties": { + "S": { + "existingJavaType": "java.lang.String", + "javaName": "symbol", + "title": "The symbol." + }, + "u": { + "existingJavaType": "java.lang.Double", + "javaName": "limitUpPrice", + "title": "The upper limit price band of a security." + }, + "d": { + "existingJavaType": "java.lang.Double", + "javaName": "limitDownPrice", + "title": "The lower limit price band of a security." + }, + "i": { + "existingJavaType": "java.lang.String", + "javaName": "indicator", + "title": "The indicator." + }, + "t": { + "existingJavaType": "java.time.OffsetDateTime", + "javaName": "timestamp", + "title": "The timestamp with nanosecond precision." + }, + "z": { + "existingJavaType": "java.lang.String", + "javaName": "tape", + "title": "The tape." + } + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/quote/stock_quote_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/quote/stock_quote_message.json new file mode 100644 index 00000000..27ff6daa --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/quote/stock_quote_message.json @@ -0,0 +1,58 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.StockMarketDataMessage" + }, + "properties": { + "S": { + "existingJavaType": "java.lang.String", + "javaName": "symbol", + "title": "The symbol." + }, + "ax": { + "existingJavaType": "java.lang.String", + "javaName": "askExchange", + "title": "The ask exchange code." + }, + "ap": { + "existingJavaType": "java.lang.Double", + "javaName": "askPrice", + "title": "The ask price." + }, + "as": { + "existingJavaType": "java.lang.Integer", + "javaName": "askSize", + "title": "The ask size." + }, + "bx": { + "existingJavaType": "java.lang.String", + "javaName": "bidExchange", + "title": "The bid exchange code." + }, + "bp": { + "existingJavaType": "java.lang.Double", + "javaName": "bidPrice", + "title": "The bid price." + }, + "bs": { + "existingJavaType": "java.lang.Integer", + "javaName": "bidSize", + "title": "The bid size." + }, + "c": { + "existingJavaType": "java.util.ArrayList", + "javaName": "conditions", + "title": "The {@link java.util.ArrayList} of quote conditions." + }, + "t": { + "existingJavaType": "java.time.OffsetDateTime", + "javaName": "timestamp", + "title": "The timestamp with nanosecond precision." + }, + "z": { + "existingJavaType": "java.lang.String", + "javaName": "tape", + "title": "The tape." + } + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/stock_market_data_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/stock_market_data_message.json new file mode 100644 index 00000000..8fbfaf6b --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/stock_market_data_message.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.model.MarketDataMessage" + }, + "properties": { + "T": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.StockMarketDataMessageType", + "javaName": "messageType", + "title": "The {@link net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.StockMarketDataMessageType}." + } + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/stock_market_data_message_type.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/stock_market_data_message_type.json new file mode 100644 index 00000000..17495738 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/stock_market_data_message_type.json @@ -0,0 +1,55 @@ +{ + "type": "string", + "enum": [ + "success", + "error", + "subscription", + "t", + "q", + "b", + "d", + "u", + "c", + "x", + "l", + "s" + ], + "javaEnums": [ + { + "name": "SUCCESS" + }, + { + "name": "ERROR" + }, + { + "name": "SUBSCRIPTION" + }, + { + "name": "TRADES" + }, + { + "name": "QUOTES" + }, + { + "name": "MINUTE_BARS" + }, + { + "name": "DAILY_BARS" + }, + { + "name": "UPDATED_BARS" + }, + { + "name": "TRADE_CORRECTIONS" + }, + { + "name": "TRADE_CANCEL_ERRORS" + }, + { + "name": "LIMIT_UP_LIMIT_DOWN_BANDS" + }, + { + "name": "TRADING_STATUSES" + } + ] +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/subscriptions/stock_subscriptions_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/subscriptions/stock_subscriptions_message.json new file mode 100644 index 00000000..986947f6 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/subscriptions/stock_subscriptions_message.json @@ -0,0 +1,49 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.StockMarketDataMessage" + }, + "properties": { + "trades": { + "existingJavaType": "java.util.ArrayList", + "title": "The {@link java.util.ArrayList} of symbols subscribed to trades." + }, + "quotes": { + "existingJavaType": "java.util.ArrayList", + "title": "The {@link java.util.ArrayList} of symbols subscribed to quotes." + }, + "bars": { + "existingJavaType": "java.util.ArrayList", + "javaName": "minuteBars", + "title": "The {@link java.util.ArrayList} of symbols subscribed to minute bars." + }, + "dailyBars": { + "existingJavaType": "java.util.ArrayList", + "title": "The {@link java.util.ArrayList} of symbols subscribed to daily bars." + }, + "updatedBars": { + "existingJavaType": "java.util.ArrayList", + "title": "The {@link java.util.ArrayList} of symbols subscribed to updated bars." + }, + "corrections": { + "existingJavaType": "java.util.ArrayList", + "javaName": "tradeCorrections", + "title": "The {@link java.util.ArrayList} of symbols subscribed to trade corrections." + }, + "cancelErrors": { + "existingJavaType": "java.util.ArrayList", + "javaName": "tradeCancelErrors", + "title": "The {@link java.util.ArrayList} of symbols subscribed to trade cancels/errors." + }, + "lulds": { + "existingJavaType": "java.util.ArrayList", + "javaName": "limitUpLimitDownBands", + "title": "The {@link java.util.ArrayList} of symbols subscribed to limit up - limit down bands." + }, + "statuses": { + "existingJavaType": "java.util.ArrayList", + "javaName": "tradingStatuses", + "title": "The {@link java.util.ArrayList} of symbols subscribed to trading statuses." + } + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/trade/stock_trade_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/trade/stock_trade_message.json new file mode 100644 index 00000000..4294856a --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/trade/stock_trade_message.json @@ -0,0 +1,48 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.StockMarketDataMessage" + }, + "properties": { + "S": { + "existingJavaType": "java.lang.String", + "javaName": "symbol", + "title": "The symbol." + }, + "i": { + "existingJavaType": "java.lang.Long", + "javaName": "tradeID", + "title": "The trade ID." + }, + "x": { + "existingJavaType": "java.lang.String", + "javaName": "exchange", + "title": "The exchange code where the trade occurred." + }, + "p": { + "existingJavaType": "java.lang.Double", + "javaName": "price", + "title": "The trade price." + }, + "s": { + "existingJavaType": "java.lang.Integer", + "javaName": "size", + "title": "The trade size." + }, + "c": { + "existingJavaType": "java.util.ArrayList", + "javaName": "conditions", + "title": "The {@link java.util.ArrayList} of trade conditions." + }, + "t": { + "existingJavaType": "java.time.OffsetDateTime", + "javaName": "timestamp", + "title": "The timestamp with nanosecond precision." + }, + "z": { + "existingJavaType": "java.lang.String", + "javaName": "tape", + "title": "The tape." + } + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/tradecancelerror/stock_trade_cancel_error_action.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/tradecancelerror/stock_trade_cancel_error_action.json new file mode 100644 index 00000000..c13371a9 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/tradecancelerror/stock_trade_cancel_error_action.json @@ -0,0 +1,15 @@ +{ + "type": "string", + "enum": [ + "C", + "E" + ], + "javaEnums": [ + { + "name": "CANCEL" + }, + { + "name": "ERROR" + } + ] +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/tradecancelerror/stock_trade_cancel_error_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/tradecancelerror/stock_trade_cancel_error_message.json new file mode 100644 index 00000000..577586c8 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/tradecancelerror/stock_trade_cancel_error_message.json @@ -0,0 +1,48 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.StockMarketDataMessage" + }, + "properties": { + "S": { + "existingJavaType": "java.lang.String", + "javaName": "symbol", + "title": "The symbol." + }, + "i": { + "existingJavaType": "java.lang.Long", + "javaName": "tradeID", + "title": "The trade ID." + }, + "x": { + "existingJavaType": "java.lang.String", + "javaName": "exchange", + "title": "The exchange code where the trade cancel/error occurred." + }, + "p": { + "existingJavaType": "java.lang.Double", + "javaName": "price", + "title": "The trade price." + }, + "s": { + "existingJavaType": "java.lang.Integer", + "javaName": "size", + "title": "The trade size." + }, + "a": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.tradecancelerror.StockTradeCancelErrorAction", + "javaName": "action", + "title": "The trade cancel/error action." + }, + "t": { + "existingJavaType": "java.time.OffsetDateTime", + "javaName": "timestamp", + "title": "The timestamp with nanosecond precision." + }, + "z": { + "existingJavaType": "java.lang.String", + "javaName": "tape", + "title": "The tape." + } + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/tradecorrection/stock_trade_correction_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/tradecorrection/stock_trade_correction_message.json new file mode 100644 index 00000000..0b6daaca --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/tradecorrection/stock_trade_correction_message.json @@ -0,0 +1,68 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.StockMarketDataMessage" + }, + "properties": { + "S": { + "existingJavaType": "java.lang.String", + "javaName": "symbol", + "title": "The symbol." + }, + "x": { + "existingJavaType": "java.lang.String", + "javaName": "exchange", + "title": "The exchange code where the trade correction occurred." + }, + "oi": { + "existingJavaType": "java.lang.Integer", + "javaName": "originalTradeID", + "title": "The original trade ID." + }, + "op": { + "existingJavaType": "java.lang.Double", + "javaName": "originalPrice", + "title": "The original trade price." + }, + "os": { + "existingJavaType": "java.lang.Integer", + "javaName": "originalSize", + "title": "The original trade size." + }, + "oc": { + "existingJavaType": "java.util.ArrayList", + "javaName": "originalConditions", + "title": "The {@link java.util.ArrayList} of original trade conditions." + }, + "ci": { + "existingJavaType": "java.lang.Integer", + "javaName": "correctedTradeID", + "title": "The corrected trade ID." + }, + "cp": { + "existingJavaType": "java.lang.Double", + "javaName": "correctedPrice", + "title": "The corrected trade price." + }, + "cs": { + "existingJavaType": "java.lang.Integer", + "javaName": "correctedSize", + "title": "The corrected trade size." + }, + "cc": { + "existingJavaType": "java.util.ArrayList", + "javaName": "correctedConditions", + "title": "The {@link java.util.ArrayList} of original trade conditions." + }, + "t": { + "existingJavaType": "java.time.OffsetDateTime", + "javaName": "timestamp", + "title": "The timestamp with nanosecond precision." + }, + "z": { + "existingJavaType": "java.lang.String", + "javaName": "tape", + "title": "The tape." + } + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/tradingstatus/stock_trading_status_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/tradingstatus/stock_trading_status_message.json new file mode 100644 index 00000000..d1946769 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/tradingstatus/stock_trading_status_message.json @@ -0,0 +1,43 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.StockMarketDataMessage" + }, + "properties": { + "S": { + "existingJavaType": "java.lang.String", + "javaName": "symbol", + "title": "The symbol." + }, + "sc": { + "existingJavaType": "java.lang.String", + "javaName": "statusCode", + "title": "The status code." + }, + "sm": { + "existingJavaType": "java.lang.String", + "javaName": "statusMessage", + "title": "The status message." + }, + "rc": { + "existingJavaType": "java.lang.String", + "javaName": "reasonCode", + "title": "The reason code." + }, + "rm": { + "existingJavaType": "java.lang.String", + "javaName": "reasonMessage", + "title": "The reason message." + }, + "t": { + "existingJavaType": "java.time.OffsetDateTime", + "javaName": "timestamp", + "title": "The timestamp with nanosecond precision." + }, + "z": { + "existingJavaType": "java.lang.String", + "javaName": "tape", + "title": "The tape." + } + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesWebsocket.java index a6fc8896..5759d355 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesWebsocket.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesWebsocket.java @@ -31,7 +31,6 @@ import static com.google.common.base.Predicates.not; import static com.google.common.collect.Iterables.toArray; import static com.google.gson.JsonParser.parseString; -import static java.lang.String.format; import static java.util.Collections.singletonList; import static java.util.concurrent.TimeUnit.SECONDS; import static net.jacobpeterson.alpaca.model.websocket.trades.model.TradesStreamMessageType.TRADE_UPDATES; @@ -127,7 +126,7 @@ protected void sendAuthenticationMessage() { } @Override - public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString byteString) { + public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString byteString) { // Binary framing final String messageString = byteString.utf8(); LOGGER.trace("Websocket message received: {}", messageString); @@ -147,11 +146,10 @@ public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString byteStri case AUTHORIZATION: message = getGson().fromJson(messageObject, AuthorizationMessage.class); final AuthorizationData authorizationData = ((AuthorizationMessage) message).getData(); - authenticated = authorizationData.getStatus().equalsIgnoreCase("authorized") && - authorizationData.getAction().equalsIgnoreCase("authenticate"); + authenticated = authorizationData.getAction().equalsIgnoreCase("authenticate") && + authorizationData.getStatus().equalsIgnoreCase("authorized"); if (!authenticated) { - throw new RuntimeException(format("%s websocket not authenticated! Received: %s.", - websocketName, message)); + throw new RuntimeException(websocketName + " websocket authentication failed!"); } else { LOGGER.info("{} websocket authenticated.", websocketName); } From 1eed19449629f8424dc581d8623e3182458ff02b Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Tue, 5 Mar 2024 09:25:23 -0800 Subject: [PATCH 62/84] WIP fix IllegalArgumentExceptions with JSON empty string values --- build.gradle | 9 ++++++--- .../jacobpeterson/alpaca/rest/broker/EventsApiSSE.java | 9 +++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index a0729cd0..b747b084 100644 --- a/build.gradle +++ b/build.gradle @@ -215,7 +215,8 @@ final def fixOpenAPIGeneratedClientIssuesTask = tasks.register("fixOpenAPIGenera .replace("this.type = this.getClass().getSimpleName();", "") // Remove the JSON validation that the generated client models always run (there isn't an option to - // disable it for some reason) + // disable it for some reason). Also, remove throwing an IllegalArgumentException for a null/empty string + // given to enum 'fromValue' methods. specIDsOfFileNames.keySet().stream().map { it.replace(".json", "") }.forEach { for (File modelFile : Paths.get(generatedClientLibrariesPath.getPath(), it, sourceFilesPath, it.replace("-", ""), "model").toFile().listFiles()) { @@ -223,9 +224,11 @@ final def fixOpenAPIGeneratedClientIssuesTask = tasks.register("fixOpenAPIGenera .replaceAll("/\\*\\*\\n {2}\\* Validates the JSON Element and throws an exception if issues " + "found[\\s\\S]*?\\*/", "") .replace("public static void validateJsonElement(JsonElement jsonElement) throws IOException {", - "public static boolean validate = false;\n public static void " + + "public static boolean validate = false;\n public static void " + "validateJsonElement(JsonElement jsonElement) throws IOException {\n" + - " if (!validate) return;") + " if (!validate) return;") + .replace("throw new IllegalArgumentException(\"Unexpected value '\" + value + \"'\");", + "return null;") } } diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/broker/EventsApiSSE.java b/src/main/java/net/jacobpeterson/alpaca/rest/broker/EventsApiSSE.java index cff5a03c..5df2e615 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/broker/EventsApiSSE.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/broker/EventsApiSSE.java @@ -19,6 +19,8 @@ import okhttp3.sse.EventSources; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.lang.reflect.Type; import java.time.LocalDate; @@ -32,6 +34,8 @@ */ public class EventsApiSSE { + private static final Logger LOGGER = LoggerFactory.getLogger(EventsApiSSE.class); + private final EventsApi eventsAPI; private final EventSource.Factory eventSourceFactory; @@ -151,18 +155,22 @@ private EventSourceListener createEventSourceListener(SSEListener sseList return new EventSourceListener() { @Override public void onClosed(@NotNull EventSource eventSource) { + LOGGER.info("Event source closed: {}", eventSource); sseListener.onClose(); } @Override public void onEvent(@NotNull EventSource eventSource, @Nullable String id, @Nullable String type, @NotNull String data) { + LOGGER.trace("Event source event: eventSource={} id={}, type={}, data={}", eventSource, id, type, data); sseListener.onMessage(getGson().fromJson(data, responseTypeToken)); } @Override public void onFailure(@NotNull EventSource eventSource, @Nullable Throwable throwable, @Nullable Response response) { + LOGGER.error("Event source failure: eventSource={} throwable={}, response={}", + eventSource, throwable, response); if (throwable != null && throwable.getMessage().equals("canceled")) { sseListener.onClose(); return; @@ -172,6 +180,7 @@ public void onFailure(@NotNull EventSource eventSource, @Nullable Throwable thro @Override public void onOpen(@NotNull EventSource eventSource, @NotNull Response response) { + LOGGER.info("Event source opened: {}", eventSource); sseListener.onOpen(); } }; From df6de0ff3ab841ba13c3d3d0fe8c30d25c987504 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Thu, 7 Mar 2024 11:50:33 -0800 Subject: [PATCH 63/84] WIP fix OpenAPI 'generate client' tasks build logic flow --- README.md | 1 + build.gradle | 35 +++++++++++++++-------------------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index afb356b9..3aa3cfcc 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ To install the built artifacts to your local Maven repository on your machine (t ``` # TODO +- Implement better reconnect logic for Websockets and SSE streaming - Implement Unit Testing for REST API and Websocket streaming (both live and mocked) # Contributing diff --git a/build.gradle b/build.gradle index b747b084..c6c53daa 100644 --- a/build.gradle +++ b/build.gradle @@ -125,17 +125,9 @@ def downloadOpenAPISpecFilesTask = tasks.register("downloadOpenAPISpecFiles") { .replace(",\"\":{\"type\":\"string\",\"x-stoplight\":{\"id\":\"1zg5jnb6rzdoo\"}}", "") } } +compileJava.dependsOn downloadOpenAPISpecFilesTask -final def generateOpenAPIClientsTask = tasks.register("generateOpenAPIClients") { - it.onlyIf { - !downloadOpenAPISpecFilesTask.get().state.upToDate && - downloadOpenAPISpecFilesTask.get().state.failure == null && - !downloadOpenAPISpecFilesTask.get().state.skipped && - downloadOpenAPISpecFilesTask.get().state.executed - } -} -compileJava.dependsOn generateOpenAPIClientsTask - +final def generateTasks = [] for (def specFileName : specIDsOfFileNames.keySet()) { final def specName = specFileName.replace(".json", "") final def inputSpecFile = new File(specDownloadPath, specFileName) @@ -185,17 +177,22 @@ for (def specFileName : specIDsOfFileNames.keySet()) { generateApiTests.set(false) generateApiDocumentation.set(false) } - generateOpenAPIClientsTask.configure { - dependsOn generateTask - } + compileJava.dependsOn generateTask + generateTasks.add(generateTask) } final def fixOpenAPIGeneratedClientIssuesTask = tasks.register("fixOpenAPIGeneratedClientIssues") { + configure { + mustRunAfter(generateTasks) + } + it.onlyIf { - !generateOpenAPIClientsTask.get().state.upToDate && - generateOpenAPIClientsTask.get().state.failure == null && - !generateOpenAPIClientsTask.get().state.skipped && - generateOpenAPIClientsTask.get().state.executed + generateTasks.stream().allMatch { generateTask -> + !generateTask.get().state.upToDate && + generateTask.get().state.failure == null && + !generateTask.get().state.skipped && + generateTask.get().state.executed + } } doLast { @@ -251,9 +248,7 @@ final def fixOpenAPIGeneratedClientIssuesTask = tasks.register("fixOpenAPIGenera } } } -generateOpenAPIClientsTask.configure { - finalizedBy fixOpenAPIGeneratedClientIssuesTask -} +compileJava.dependsOn fixOpenAPIGeneratedClientIssuesTask // // END Alpaca OpenAPI Specification (OAS) client generation From ec35299fe7287c7bd5a3797e6e92f871b5bd3827 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Thu, 7 Mar 2024 18:06:34 -0800 Subject: [PATCH 64/84] Finish updating websocket interface and functionality --- .../net/jacobpeterson/alpaca/AlpacaAPI.java | 106 ++++++--- .../alpaca/AlpacaMarketDataAPI.java | 1 - .../alpaca/rest/broker/EventsApiSSE.java | 2 +- ...=> market_data_websocket_source_type.json} | 1 - .../alpaca/util/sse/SSEListenerAdapter.java | 4 +- .../alpaca/websocket/AlpacaWebsocket.java | 89 +++---- .../websocket/AlpacaWebsocketInterface.java | 24 +- .../AlpacaWebsocketMessageListener.java | 20 -- .../marketdata/MarketDataWebsocket.java | 198 ++++++++++++++++ .../MarketDataWebsocketInterface.java | 8 + .../crypto/CryptoMarketDataListener.java | 55 +++++ .../CryptoMarketDataListenerAdapter.java | 30 +++ .../crypto/CryptoMarketDataWebsocket.java | 174 ++++++++++++-- .../CryptoMarketDataWebsocketInterface.java | 116 +++++++++ .../control/crypto_subscriptions_message.json | 34 +++ ...ntry.json => crypto_order_book_entry.json} | 0 .../orderbook/crypto_order_book_message.json | 28 +++ .../orderbook/crypto_orderbook_message.json | 28 --- .../crypto_subscriptions_message.json | 33 --- .../streams/news/NewsMarketDataListener.java | 17 ++ .../news/NewsMarketDataListenerAdapter.java | 12 + .../streams/news/NewsMarketDataWebsocket.java | 86 ++++++- .../NewsMarketDataWebsocketInterface.java | 36 +++ .../control/news_subscriptions_message.json | 12 + .../streams/news/model/news/news_message.json | 2 +- .../stock/StockMarketDataListener.java | 78 +++++++ .../stock/StockMarketDataListenerAdapter.java | 42 ++++ .../stock/StockMarketDataWebsocket.java | 220 ++++++++++++++++-- .../StockMarketDataWebsocketInterface.java | 136 +++++++++++ .../control/stock_subscriptions_message.json | 39 ++++ .../model/quote/stock_quote_message.json | 4 +- .../stock_subscriptions_message.json | 49 ---- .../model/trade/stock_trade_message.json | 4 +- .../stock_trade_correction_message.json | 8 +- .../websocket/trades/TradesListener.java | 11 - .../trades/TradesListenerAdapter.java | 19 -- .../websocket/trades/TradesWebsocket.java | 217 ----------------- .../trades/TradesWebsocketInterface.java | 26 --- .../model/listening/listening_data.json | 9 - .../websocket/updates/UpdatesListener.java | 17 ++ .../websocket/updates/UpdatesWebsocket.java | 172 ++++++++++++++ .../updates/UpdatesWebsocketInterface.java | 24 ++ .../authorization/authorization_data.json | 0 .../authorization/authorization_message.json | 6 +- .../model/listening/listening_data.json | 9 + .../model/listening/listening_message.json | 6 +- .../model/tradeupdate/trade_update.json | 4 +- .../model/tradeupdate/trade_update_event.json | 0 .../tradeupdate/trade_update_message.json | 6 +- .../model/updates_message.json} | 4 +- .../model/updates_message_type.json} | 0 51 files changed, 1658 insertions(+), 568 deletions(-) rename src/main/java/net/jacobpeterson/alpaca/util/apitype/{market_data_api_stream_source_type.json => market_data_websocket_source_type.json} (56%) delete mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketMessageListener.java create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocketInterface.java create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/CryptoMarketDataListener.java create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/CryptoMarketDataListenerAdapter.java create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/CryptoMarketDataWebsocketInterface.java create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/control/crypto_subscriptions_message.json rename src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/orderbook/{crypto_orderbook_entry.json => crypto_order_book_entry.json} (100%) create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/orderbook/crypto_order_book_message.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/orderbook/crypto_orderbook_message.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/subscriptions/crypto_subscriptions_message.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/NewsMarketDataListener.java create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/NewsMarketDataListenerAdapter.java create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/NewsMarketDataWebsocketInterface.java create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/model/control/news_subscriptions_message.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/StockMarketDataListener.java create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/StockMarketDataListenerAdapter.java create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/StockMarketDataWebsocketInterface.java create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/control/stock_subscriptions_message.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/subscriptions/stock_subscriptions_message.json delete mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesListener.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesListenerAdapter.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesWebsocket.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesWebsocketInterface.java delete mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/listening/listening_data.json create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/updates/UpdatesListener.java create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/updates/UpdatesWebsocket.java create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/updates/UpdatesWebsocketInterface.java rename src/main/java/net/jacobpeterson/alpaca/websocket/{trades => updates}/model/authorization/authorization_data.json (100%) rename src/main/java/net/jacobpeterson/alpaca/websocket/{trades => updates}/model/authorization/authorization_message.json (66%) create mode 100644 src/main/java/net/jacobpeterson/alpaca/websocket/updates/model/listening/listening_data.json rename src/main/java/net/jacobpeterson/alpaca/websocket/{trades => updates}/model/listening/listening_message.json (69%) rename src/main/java/net/jacobpeterson/alpaca/websocket/{trades => updates}/model/tradeupdate/trade_update.json (90%) rename src/main/java/net/jacobpeterson/alpaca/websocket/{trades => updates}/model/tradeupdate/trade_update_event.json (100%) rename src/main/java/net/jacobpeterson/alpaca/websocket/{trades => updates}/model/tradeupdate/trade_update_message.json (69%) rename src/main/java/net/jacobpeterson/alpaca/websocket/{trades/model/trades_stream_message.json => updates/model/updates_message.json} (69%) rename src/main/java/net/jacobpeterson/alpaca/websocket/{trades/model/trades_stream_message_type.json => updates/model/updates_message_type.json} (100%) diff --git a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java index 001aa617..2ae2e22e 100644 --- a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java +++ b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java @@ -1,17 +1,23 @@ package net.jacobpeterson.alpaca; import net.jacobpeterson.alpaca.model.util.apitype.BrokerAPIEndpointType; -import net.jacobpeterson.alpaca.model.util.apitype.MarketDataAPIStreamSourceType; +import net.jacobpeterson.alpaca.model.util.apitype.MarketDataWebsocketSourceType; import net.jacobpeterson.alpaca.model.util.apitype.TraderAPIEndpointType; -import net.jacobpeterson.alpaca.websocket.trades.TradesWebsocket; -import net.jacobpeterson.alpaca.websocket.trades.TradesWebsocketInterface; +import net.jacobpeterson.alpaca.websocket.marketdata.streams.crypto.CryptoMarketDataWebsocket; +import net.jacobpeterson.alpaca.websocket.marketdata.streams.crypto.CryptoMarketDataWebsocketInterface; +import net.jacobpeterson.alpaca.websocket.marketdata.streams.news.NewsMarketDataWebsocket; +import net.jacobpeterson.alpaca.websocket.marketdata.streams.news.NewsMarketDataWebsocketInterface; +import net.jacobpeterson.alpaca.websocket.marketdata.streams.stock.StockMarketDataWebsocket; +import net.jacobpeterson.alpaca.websocket.marketdata.streams.stock.StockMarketDataWebsocketInterface; +import net.jacobpeterson.alpaca.websocket.updates.UpdatesWebsocket; +import net.jacobpeterson.alpaca.websocket.updates.UpdatesWebsocketInterface; import okhttp3.OkHttpClient; import okhttp3.logging.HttpLoggingInterceptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static net.jacobpeterson.alpaca.model.util.apitype.BrokerAPIEndpointType.SANDBOX; -import static net.jacobpeterson.alpaca.model.util.apitype.MarketDataAPIStreamSourceType.IEX; +import static net.jacobpeterson.alpaca.model.util.apitype.MarketDataWebsocketSourceType.IEX; import static net.jacobpeterson.alpaca.model.util.apitype.TraderAPIEndpointType.PAPER; import static okhttp3.logging.HttpLoggingInterceptor.Level.BODY; @@ -33,7 +39,7 @@ public class AlpacaAPI { private final String traderSecretKey; private final String traderOAuthToken; private final TraderAPIEndpointType traderAPIEndpointType; - private final MarketDataAPIStreamSourceType marketDataAPIStreamSourceType; + private final MarketDataWebsocketSourceType marketDataWebsocketSourceType; private final String brokerAPIKey; private final String brokerAPISecret; private final BrokerAPIEndpointType brokerAPIEndpointType; @@ -42,7 +48,10 @@ public class AlpacaAPI { private AlpacaTraderAPI trader; private AlpacaMarketDataAPI marketData; private AlpacaBrokerAPI broker; - private TradesWebsocket tradesWebsocket; + private UpdatesWebsocket updatesWebsocket; + private StockMarketDataWebsocket stockMarketDataWebsocket; + private CryptoMarketDataWebsocket cryptoMarketDataWebsocket; + private NewsMarketDataWebsocket newsMarketDataWebsocket; /** * Instantiates a new {@link AlpacaAPI}. Use this constructor if you are using the Trading or Market Data APIs for a @@ -51,11 +60,11 @@ public class AlpacaAPI { * @param traderKeyID the Trader key ID * @param traderSecretKey the Trader secret key * @param traderAPIEndpointType the {@link TraderAPIEndpointType} - * @param marketDataAPIStreamSourceType the {@link MarketDataAPIStreamSourceType} + * @param marketDataWebsocketSourceType the {@link MarketDataWebsocketSourceType} */ public AlpacaAPI(String traderKeyID, String traderSecretKey, TraderAPIEndpointType traderAPIEndpointType, - MarketDataAPIStreamSourceType marketDataAPIStreamSourceType) { - this(traderKeyID, traderSecretKey, null, traderAPIEndpointType, marketDataAPIStreamSourceType, null, null, null, + MarketDataWebsocketSourceType marketDataWebsocketSourceType) { + this(traderKeyID, traderSecretKey, null, traderAPIEndpointType, marketDataWebsocketSourceType, null, null, null, null); } @@ -66,13 +75,13 @@ public AlpacaAPI(String traderKeyID, String traderSecretKey, TraderAPIEndpointTy * @param traderKeyID the Trader key ID * @param traderSecretKey the Trader secret key * @param traderAPIEndpointType the {@link TraderAPIEndpointType} - * @param marketDataAPIStreamSourceType the {@link MarketDataAPIStreamSourceType} + * @param marketDataWebsocketSourceType the {@link MarketDataWebsocketSourceType} * @param okHttpClient an existing {@link OkHttpClient} or null to create a new * default instance */ public AlpacaAPI(String traderKeyID, String traderSecretKey, TraderAPIEndpointType traderAPIEndpointType, - MarketDataAPIStreamSourceType marketDataAPIStreamSourceType, OkHttpClient okHttpClient) { - this(traderKeyID, traderSecretKey, null, traderAPIEndpointType, marketDataAPIStreamSourceType, null, null, null, + MarketDataWebsocketSourceType marketDataWebsocketSourceType, OkHttpClient okHttpClient) { + this(traderKeyID, traderSecretKey, null, traderAPIEndpointType, marketDataWebsocketSourceType, null, null, null, okHttpClient); } @@ -133,7 +142,7 @@ public AlpacaAPI(String brokerAPIKey, String brokerAPISecret, BrokerAPIEndpointT * @param traderSecretKey the Trader secret key * @param traderOAuthToken the Trader OAuth token * @param traderAPIEndpointType the {@link TraderAPIEndpointType} - * @param marketDataAPIStreamSourceType the {@link MarketDataAPIStreamSourceType} + * @param marketDataWebsocketSourceType the {@link MarketDataWebsocketSourceType} * @param brokerAPIKey the Broker API key * @param brokerAPISecret the Broker API secret * @param brokerAPIEndpointType the {@link BrokerAPIEndpointType} @@ -142,18 +151,20 @@ public AlpacaAPI(String brokerAPIKey, String brokerAPISecret, BrokerAPIEndpointT */ public AlpacaAPI(String traderKeyID, String traderSecretKey, String traderOAuthToken, TraderAPIEndpointType traderAPIEndpointType, - MarketDataAPIStreamSourceType marketDataAPIStreamSourceType, + MarketDataWebsocketSourceType marketDataWebsocketSourceType, String brokerAPIKey, String brokerAPISecret, BrokerAPIEndpointType brokerAPIEndpointType, OkHttpClient okHttpClient) { this.traderKeyID = traderKeyID; this.traderSecretKey = traderSecretKey; this.traderOAuthToken = traderOAuthToken; - this.traderAPIEndpointType = traderAPIEndpointType != null ? traderAPIEndpointType : PAPER; - this.marketDataAPIStreamSourceType = marketDataAPIStreamSourceType != null ? - marketDataAPIStreamSourceType : IEX; + this.traderAPIEndpointType = traderAPIEndpointType != null ? + traderAPIEndpointType : PAPER; + this.marketDataWebsocketSourceType = marketDataWebsocketSourceType != null ? + marketDataWebsocketSourceType : IEX; this.brokerAPIKey = brokerAPIKey; this.brokerAPISecret = brokerAPISecret; - this.brokerAPIEndpointType = brokerAPIEndpointType != null ? brokerAPIEndpointType : SANDBOX; + this.brokerAPIEndpointType = brokerAPIEndpointType != null ? + brokerAPIEndpointType : SANDBOX; // Create default OkHttpClient instance if (okHttpClient == null) { @@ -224,16 +235,55 @@ public synchronized AlpacaBrokerAPI broker() { } /** - * Gets the {@link TradesWebsocketInterface}. Lazily instantiated. + * Gets the {@link UpdatesWebsocketInterface}. Lazily instantiated. * - * @return the {@link TradesWebsocketInterface} + * @return the {@link UpdatesWebsocketInterface} */ - public synchronized TradesWebsocketInterface tradesStream() { - if (tradesWebsocket == null) { - tradesWebsocket = new TradesWebsocket(okHttpClient, traderAPIEndpointType, + public synchronized UpdatesWebsocketInterface updatesStream() { + if (updatesWebsocket == null) { + updatesWebsocket = new UpdatesWebsocket(okHttpClient, traderAPIEndpointType, traderKeyID, traderSecretKey, traderOAuthToken); } - return tradesWebsocket; + return updatesWebsocket; + } + + /** + * Gets the {@link StockMarketDataWebsocketInterface}. Lazily instantiated. + * + * @return the {@link StockMarketDataWebsocketInterface} + */ + public synchronized StockMarketDataWebsocketInterface stockMarketDataStream() { + if (stockMarketDataWebsocket == null) { + stockMarketDataWebsocket = new StockMarketDataWebsocket(okHttpClient, + traderKeyID, traderSecretKey, brokerAPIKey, brokerAPISecret, marketDataWebsocketSourceType); + } + return stockMarketDataWebsocket; + } + + /** + * Gets the {@link CryptoMarketDataWebsocketInterface}. Lazily instantiated. + * + * @return the {@link CryptoMarketDataWebsocketInterface} + */ + public synchronized CryptoMarketDataWebsocketInterface cryptoMarketDataStream() { + if (cryptoMarketDataWebsocket == null) { + cryptoMarketDataWebsocket = new CryptoMarketDataWebsocket(okHttpClient, + traderKeyID, traderSecretKey, brokerAPIKey, brokerAPISecret); + } + return cryptoMarketDataWebsocket; + } + + /** + * Gets the {@link NewsMarketDataWebsocketInterface}. Lazily instantiated. + * + * @return the {@link NewsMarketDataWebsocketInterface} + */ + public synchronized NewsMarketDataWebsocketInterface newsMarketDataStream() { + if (newsMarketDataWebsocket == null) { + newsMarketDataWebsocket = new NewsMarketDataWebsocket(okHttpClient, + traderKeyID, traderSecretKey, brokerAPIKey, brokerAPISecret); + } + return newsMarketDataWebsocket; } /** @@ -254,7 +304,7 @@ public static final class Builder { private String traderSecretKey; private String traderOAuthToken; private TraderAPIEndpointType traderAPIEndpointType; - private MarketDataAPIStreamSourceType marketDataAPISourceType; + private MarketDataWebsocketSourceType marketDataWebsocketSourceType; private String brokerAPIKey; private String brokerAPISecret; private BrokerAPIEndpointType brokerAPIEndpointType; @@ -282,8 +332,8 @@ public Builder withTraderAPIEndpointType(TraderAPIEndpointType traderAPIEndpoint return this; } - public Builder withMarketDataAPIStreamSourceType(MarketDataAPIStreamSourceType marketDataAPISourceType) { - this.marketDataAPISourceType = marketDataAPISourceType; + public Builder withMarketDataWebsocketSourceType(MarketDataWebsocketSourceType marketDataWebsocketSourceType) { + this.marketDataWebsocketSourceType = marketDataWebsocketSourceType; return this; } @@ -309,7 +359,7 @@ public Builder withOkHttpClient(OkHttpClient okHttpClient) { public AlpacaAPI build() { return new AlpacaAPI(traderKeyID, traderSecretKey, traderOAuthToken, traderAPIEndpointType, - marketDataAPISourceType, brokerAPIKey, brokerAPISecret, brokerAPIEndpointType, okHttpClient); + marketDataWebsocketSourceType, brokerAPIKey, brokerAPISecret, brokerAPIEndpointType, okHttpClient); } } } diff --git a/src/main/java/net/jacobpeterson/alpaca/AlpacaMarketDataAPI.java b/src/main/java/net/jacobpeterson/alpaca/AlpacaMarketDataAPI.java index 65682a4e..1728c09f 100644 --- a/src/main/java/net/jacobpeterson/alpaca/AlpacaMarketDataAPI.java +++ b/src/main/java/net/jacobpeterson/alpaca/AlpacaMarketDataAPI.java @@ -46,7 +46,6 @@ public class AlpacaMarketDataAPI { checkNotNull(okHttpClient); final boolean traderKeysGiven = traderKeyID != null && traderSecretKey != null; - apiClient = new ApiClient(okHttpClient); apiClient.setServerIndex(traderKeysGiven ? 0 : 1); if (traderKeysGiven) { diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/broker/EventsApiSSE.java b/src/main/java/net/jacobpeterson/alpaca/rest/broker/EventsApiSSE.java index 5df2e615..ea2341e5 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/broker/EventsApiSSE.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/broker/EventsApiSSE.java @@ -155,7 +155,7 @@ private EventSourceListener createEventSourceListener(SSEListener sseList return new EventSourceListener() { @Override public void onClosed(@NotNull EventSource eventSource) { - LOGGER.info("Event source closed: {}", eventSource); + LOGGER.info("Event source closed: eventSource={}", eventSource); sseListener.onClose(); } diff --git a/src/main/java/net/jacobpeterson/alpaca/util/apitype/market_data_api_stream_source_type.json b/src/main/java/net/jacobpeterson/alpaca/util/apitype/market_data_websocket_source_type.json similarity index 56% rename from src/main/java/net/jacobpeterson/alpaca/util/apitype/market_data_api_stream_source_type.json rename to src/main/java/net/jacobpeterson/alpaca/util/apitype/market_data_websocket_source_type.json index 97b87531..5a27c8fb 100644 --- a/src/main/java/net/jacobpeterson/alpaca/util/apitype/market_data_api_stream_source_type.json +++ b/src/main/java/net/jacobpeterson/alpaca/util/apitype/market_data_websocket_source_type.json @@ -1,6 +1,5 @@ { "type": "string", - "javaName": "MarketDataAPIStreamSourceType", "enum": [ "iex", "sip" diff --git a/src/main/java/net/jacobpeterson/alpaca/util/sse/SSEListenerAdapter.java b/src/main/java/net/jacobpeterson/alpaca/util/sse/SSEListenerAdapter.java index 57e33dfb..0bd194d1 100644 --- a/src/main/java/net/jacobpeterson/alpaca/util/sse/SSEListenerAdapter.java +++ b/src/main/java/net/jacobpeterson/alpaca/util/sse/SSEListenerAdapter.java @@ -24,10 +24,10 @@ public void onClose() { } public void onError(@Nullable Throwable throwable, @Nullable Response response) { - LOGGER.error("SSE connection error! {}", response, throwable); + LOGGER.error("SSE connection error! response={}", response, throwable); } public void onMessage(@NotNull T message) { - LOGGER.info("SSE message received: {}", message); + LOGGER.info("SSE message received: message={}", message); } } diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java index d475cc8a..518435c2 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java @@ -20,13 +20,8 @@ /** * {@link AlpacaWebsocket} represents an abstract websocket for Alpaca. - * - * @param the {@link AlpacaWebsocketMessageListener} type parameter - * @param the 'message type' type parameter - * @param the 'message' type parameter */ -public abstract class AlpacaWebsocket> extends WebSocketListener - implements AlpacaWebsocketInterface { +public abstract class AlpacaWebsocket extends WebSocketListener implements AlpacaWebsocketInterface { /** * Defines a websocket normal closure code. @@ -45,7 +40,7 @@ public abstract class AlpacaWebsocket authenticationMessageFuture; @@ -78,12 +68,8 @@ public abstract class AlpacaWebsocket { @@ -146,7 +125,6 @@ public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) { onConnection(); } }); - if (alpacaWebsocketStateListener != null) { alpacaWebsocketStateListener.onOpen(response); } @@ -155,16 +133,13 @@ public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) { @Override public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) { connected = false; - if (intentionalClose) { - LOGGER.info("{} websocket closed.", websocketName); - LOGGER.debug("Close code: {}, Reason: {}", code, reason); + LOGGER.info("{} websocket closed. code={}, reason={}", websocketName, code, reason); cleanupState(); } else { - LOGGER.error("{} websocket closed unintentionally! Code: {}, Reason: {}", websocketName, code, reason); + LOGGER.error("{} websocket closed unintentionally! code={}, reason={}", websocketName, code, reason); handleReconnectionAttempt(); } - if (alpacaWebsocketStateListener != null) { alpacaWebsocketStateListener.onClosed(code, reason); } @@ -173,13 +148,11 @@ public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String rea @Override public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable cause, @Nullable Response response) { LOGGER.error("{} websocket failure!", websocketName, cause); - // A websocket failure occurs when either there is a connection failure or when the client throws // an exception when receiving a message. In either case, OkHttp will close the websocket connection, // so try to reopen it. connected = false; handleReconnectionAttempt(); - if (alpacaWebsocketStateListener != null) { alpacaWebsocketStateListener.onFailure(cause); } @@ -225,6 +198,16 @@ protected void cleanupState() { reconnectAttempts = 0; } + /** + * Sends a message to the underlying {@link #websocket}. + * + * @param message the message + */ + protected void sendWebsocketMessage(String message) { + LOGGER.trace("Websocket message sent: {}", message); + websocket.send(message); + } + /** * Called asynchronously when a websocket connection is made. */ @@ -248,40 +231,30 @@ public Future getAuthorizationFuture() { return authenticationMessageFuture; } - /** - * Calls the {@link AlpacaWebsocketMessageListener}. - * - * @param messageType the message type - * @param message the message - */ - protected void callListener(T messageType, M message) { - if (listener != null) { - try { - listener.onMessage(messageType, message); - } catch (Exception exception) { - LOGGER.error("{} listener threw exception!", websocketName, exception); - } - } - } - @Override - public void setListener(L listener) { - this.listener = listener; - } - - public AlpacaWebsocketStateListener getWebsocketStateListener() { - return alpacaWebsocketStateListener; - } - public void setAlpacaWebsocketStateListener(AlpacaWebsocketStateListener alpacaWebsocketStateListener) { this.alpacaWebsocketStateListener = alpacaWebsocketStateListener; } + @Override public boolean doesAutomaticallyReconnect() { return automaticallyReconnect; } + @Override public void setAutomaticallyReconnect(boolean automaticallyReconnect) { this.automaticallyReconnect = automaticallyReconnect; } + + public OkHttpClient getOkHttpClient() { + return okHttpClient; + } + + public HttpUrl getWebsocketURL() { + return websocketURL; + } + + public WebSocket getWebsocket() { + return websocket; + } } diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketInterface.java b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketInterface.java index 033475a2..48adfb4d 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketInterface.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketInterface.java @@ -7,10 +7,8 @@ /** * {@link AlpacaWebsocketInterface} defines an interface for Alpaca websockets. - * - * @param the {@link AlpacaWebsocketMessageListener} type parameter */ -public interface AlpacaWebsocketInterface> { +public interface AlpacaWebsocketInterface { /** * Connects this Websocket. @@ -74,9 +72,23 @@ default boolean waitForAuthorization(long timeout, TimeUnit unit) { } /** - * Sets the {@link AlpacaWebsocketMessageListener} for this {@link AlpacaWebsocketInterface}. + * Sets the {@link AlpacaWebsocketStateListener}. * - * @param listener the {@link AlpacaWebsocketMessageListener} + * @param alpacaWebsocketStateListener the {@link AlpacaWebsocketStateListener} */ - void setListener(L listener); + void setAlpacaWebsocketStateListener(AlpacaWebsocketStateListener alpacaWebsocketStateListener); + + /** + * Returns true if this websocket automatically reconnects, false otherwise. + * + * @return a boolean + */ + boolean doesAutomaticallyReconnect(); + + /** + * Sets whether to automatically reconnect and reauthenticate on a websocket failure. true by default. + * + * @param automaticallyReconnect true to automatically reconnect, false otherwise + */ + void setAutomaticallyReconnect(boolean automaticallyReconnect); } diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketMessageListener.java b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketMessageListener.java deleted file mode 100644 index da55535c..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocketMessageListener.java +++ /dev/null @@ -1,20 +0,0 @@ -package net.jacobpeterson.alpaca.websocket; - -/** - * {@link AlpacaWebsocketMessageListener} defines a listener interface for messages from an {@link AlpacaWebsocket} - * instances. - * - * @param the 'message type' type parameter - * @param the 'message' type parameter - */ -@FunctionalInterface -public interface AlpacaWebsocketMessageListener { - - /** - * Called when a message is received. - * - * @param messageType the message type - * @param message the message - */ - void onMessage(T messageType, M message); -} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java new file mode 100644 index 00000000..fbb7dca9 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java @@ -0,0 +1,198 @@ +package net.jacobpeterson.alpaca.websocket.marketdata; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import net.jacobpeterson.alpaca.model.websocket.marketdata.model.MarketDataMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.model.control.ErrorMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.model.control.SuccessMessage; +import net.jacobpeterson.alpaca.websocket.AlpacaWebsocket; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.WebSocket; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Set; +import java.util.function.Function; + +import static com.google.common.collect.Sets.difference; +import static com.google.gson.JsonParser.parseString; +import static java.util.concurrent.TimeUnit.SECONDS; +import static net.jacobpeterson.alpaca.model.websocket.marketdata.model.control.SuccessMessageType.AUTHENTICATED; +import static net.jacobpeterson.alpaca.openapi.marketdata.JSON.getGson; + +/** + * {@link MarketDataWebsocket} is an abstract {@link AlpacaWebsocket} implementation for + * Market Data Streaming. + * + * @param the 'message type' type + * @param the 'subscription message' type + * @param the 'listener' type + */ +public abstract class MarketDataWebsocket extends AlpacaWebsocket + implements MarketDataWebsocketInterface { + + private static final Logger LOGGER = LoggerFactory.getLogger(MarketDataWebsocket.class); + private static final Set AUTH_FAILURE_MESSAGES = Set.of("auth failed", "auth timeout", "not authenticated"); + + protected final String authKey; + protected final String authSecret; + protected final Class messageTypeClass; + protected final Class subscriptionsMessageClass; + protected S subscriptionsMessage; + protected L listener; + + /** + * Instantiates a new {@link MarketDataWebsocket}. + * + * @param okHttpClient the {@link OkHttpClient} + * @param websocketURL the websocket {@link HttpUrl} + * @param websocketMarketDataTypeName the websocket market data type name {@link String} + * @param traderKeyID the trader key ID + * @param traderSecretKey the trader secret key + * @param brokerAPIKey the broker API key + * @param brokerAPISecret the broker API secret + * @param messageTypeClass the {@link T} message type {@link Class} + * @param subscriptionsMessageClass the {@link S} subscription message {@link Class} + */ + protected MarketDataWebsocket(OkHttpClient okHttpClient, HttpUrl websocketURL, String websocketMarketDataTypeName, + String traderKeyID, String traderSecretKey, String brokerAPIKey, String brokerAPISecret, + Class messageTypeClass, Class subscriptionsMessageClass) { + super(okHttpClient, websocketURL, websocketMarketDataTypeName + " Market Data"); + final boolean traderKeysGiven = traderKeyID != null && traderSecretKey != null; + this.authKey = traderKeysGiven ? traderKeyID : brokerAPIKey; + this.authSecret = traderKeysGiven ? traderSecretKey : brokerAPISecret; + this.messageTypeClass = messageTypeClass; + this.subscriptionsMessageClass = subscriptionsMessageClass; + } + + @Override + protected void cleanupState() { + super.cleanupState(); + subscriptionsMessage = null; + } + + @Override + protected void onConnection() { + sendAuthenticationMessage(); + } + + @Override + protected void onReconnection() { + sendAuthenticationMessage(); + if (waitForAuthorization(5, SECONDS) && subscriptionsMessage != null) { + sendSubscriptionMessage(subscriptionsMessage, true); + } + } + + @Override + protected void sendAuthenticationMessage() { + // Ensure that the authorization Future exists + getAuthorizationFuture(); + + final JsonObject authObject = new JsonObject(); + authObject.addProperty("action", "auth"); + authObject.addProperty("key", authKey); + authObject.addProperty("secret", authSecret); + + LOGGER.info("{} websocket sending authentication message...", websocketName); + sendWebsocketMessage(authObject.toString()); + } + + @Override + public void onMessage(@NotNull WebSocket webSocket, @NotNull String message) { // Text framing + LOGGER.trace("Websocket message received: {}", message); + + // Loop through message array and handle each message according to its type + for (JsonElement arrayElement : parseString(message).getAsJsonArray()) { + final JsonObject messageObject = arrayElement.getAsJsonObject(); + final T messageType = getGson().fromJson(messageObject.get("T"), messageTypeClass); + if (isSuccessMessageType(messageType)) { + final SuccessMessage successMessage = getGson().fromJson(messageObject, SuccessMessage.class); + if (successMessage.getMessageType() == AUTHENTICATED) { + LOGGER.info("{} websocket authenticated.", websocketName); + authenticated = true; + if (authenticationMessageFuture != null) { + authenticationMessageFuture.complete(true); + } + } + } else if (isErrorMessageType(messageType)) { + final ErrorMessage errorMessage = getGson().fromJson(messageObject, ErrorMessage.class); + if (AUTH_FAILURE_MESSAGES.contains(errorMessage.getMessage()) && authenticationMessageFuture != null) { + authenticated = false; + authenticationMessageFuture.complete(false); + throw new RuntimeException(websocketName + " websocket authentication failed!"); + } else { + throw new RuntimeException(websocketName + " websocket error! Message: " + errorMessage); + } + } else if (isSubscriptionMessageType(messageType)) { + subscriptionsMessage = getGson().fromJson(messageObject, subscriptionsMessageClass); + } else if (listener != null) { + callListenerWithMessage(messageType, messageObject); + } + } + } + + /** + * Sets the websocket stream's subscriptions for a specific message type. + * + * @param previousSubscriptions the previous subscriptions symbol {@link Set} + * @param newSubscriptions the new subscriptions symbol {@link Set} + * @param subscriptionUpdateObjectCreator the subscription update object creator {@link Function} + */ + protected void setSubscriptions(@NotNull Set previousSubscriptions, + @NotNull Set newSubscriptions, @NotNull Function, S> subscriptionUpdateObjectCreator) { + // Unsubscribe from previous subscriptions + final Set unsubscribeSet = difference(previousSubscriptions, newSubscriptions); + if (!unsubscribeSet.isEmpty()) { + sendSubscriptionMessage(subscriptionUpdateObjectCreator.apply(unsubscribeSet), false); + } + // Subscribe to new subscriptions + final Set subscribeSet = difference(newSubscriptions, previousSubscriptions); + if (!subscribeSet.isEmpty()) { + sendSubscriptionMessage(subscriptionUpdateObjectCreator.apply(subscribeSet), true); + } + } + + private void sendSubscriptionMessage(S subscriptionsMessage, boolean subscribe) { + final JsonObject subscribeObject = getGson().toJsonTree(subscriptionsMessage).getAsJsonObject(); + subscribeObject.addProperty("action", subscribe ? "subscribe" : "unsubscribe"); + sendWebsocketMessage(getGson().toJson(subscribeObject)); + } + + /** + * Whether the given messageType is "success". + * + * @param messageType the message type + * + * @return a boolean + */ + protected abstract boolean isSuccessMessageType(T messageType); + + /** + * Whether the given messageType is "error". + * + * @param messageType the message type + * + * @return a boolean + */ + protected abstract boolean isErrorMessageType(T messageType); + + /** + * Whether the given messageType is "subscription". + * + * @param messageType the message type + * + * @return a boolean + */ + protected abstract boolean isSubscriptionMessageType(T messageType); + + /** + * Calls the {@link #listener} with a {@link MarketDataMessage} from a {@link JsonObject}. + * + * @param messageType the message type + * @param messageObject the message {@link JsonObject} + */ + protected abstract void callListenerWithMessage(T messageType, JsonObject messageObject); +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocketInterface.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocketInterface.java new file mode 100644 index 00000000..989389eb --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocketInterface.java @@ -0,0 +1,8 @@ +package net.jacobpeterson.alpaca.websocket.marketdata; + +import net.jacobpeterson.alpaca.websocket.AlpacaWebsocketInterface; + +/** + * {@link MarketDataWebsocketInterface} is an {@link AlpacaWebsocketInterface} for {@link MarketDataWebsocket}. + */ +public interface MarketDataWebsocketInterface extends AlpacaWebsocketInterface {} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/CryptoMarketDataListener.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/CryptoMarketDataListener.java new file mode 100644 index 00000000..ae8efe52 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/CryptoMarketDataListener.java @@ -0,0 +1,55 @@ +package net.jacobpeterson.alpaca.websocket.marketdata.streams.crypto; + +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.bar.CryptoBarMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.orderbook.CryptoOrderBookMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.quote.CryptoQuoteMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.trade.CryptoTradeMessage; + +/** + * {@link CryptoMarketDataListener} defines a listener interface for {@link CryptoMarketDataWebsocketInterface} + * messages. + */ +public interface CryptoMarketDataListener { + + /** + * Called when a {@link CryptoTradeMessage} is received. + * + * @param trade the {@link CryptoTradeMessage} + */ + void onTrade(CryptoTradeMessage trade); + + /** + * Called when a {@link CryptoQuoteMessage} is received. + * + * @param quote the {@link CryptoQuoteMessage} + */ + void onQuote(CryptoQuoteMessage quote); + + /** + * Called when a {@link CryptoBarMessage} is received. + * + * @param bar the {@link CryptoBarMessage} + */ + void onMinuteBar(CryptoBarMessage bar); + + /** + * Called when a {@link CryptoBarMessage} is received. + * + * @param bar the {@link CryptoBarMessage} + */ + void onDailyBar(CryptoBarMessage bar); + + /** + * Called when a {@link CryptoBarMessage} is received. + * + * @param bar the {@link CryptoBarMessage} + */ + void onUpdatedBar(CryptoBarMessage bar); + + /** + * Called when a {@link CryptoOrderBookMessage} is received. + * + * @param orderBook the {@link CryptoOrderBookMessage} + */ + void onOrderBook(CryptoOrderBookMessage orderBook); +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/CryptoMarketDataListenerAdapter.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/CryptoMarketDataListenerAdapter.java new file mode 100644 index 00000000..cf1cc348 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/CryptoMarketDataListenerAdapter.java @@ -0,0 +1,30 @@ +package net.jacobpeterson.alpaca.websocket.marketdata.streams.crypto; + +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.bar.CryptoBarMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.orderbook.CryptoOrderBookMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.quote.CryptoQuoteMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.trade.CryptoTradeMessage; + +/** + * {@link CryptoMarketDataListenerAdapter} is an adapter for {@link CryptoMarketDataListener}. + */ +public class CryptoMarketDataListenerAdapter implements CryptoMarketDataListener { + + @Override + public void onTrade(CryptoTradeMessage trade) {} + + @Override + public void onQuote(CryptoQuoteMessage quote) {} + + @Override + public void onMinuteBar(CryptoBarMessage bar) {} + + @Override + public void onDailyBar(CryptoBarMessage bar) {} + + @Override + public void onUpdatedBar(CryptoBarMessage bar) {} + + @Override + public void onOrderBook(CryptoOrderBookMessage orderBook) {} +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/CryptoMarketDataWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/CryptoMarketDataWebsocket.java index 8eed2ce2..ed3a88ad 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/CryptoMarketDataWebsocket.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/CryptoMarketDataWebsocket.java @@ -1,27 +1,175 @@ package net.jacobpeterson.alpaca.websocket.marketdata.streams.crypto; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.realtime.bar.CryptoBarMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.realtime.quote.CryptoQuoteMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.crypto.realtime.trade.CryptoTradeMessage; +import com.google.gson.JsonObject; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.CryptoMarketDataMessageType; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.bar.CryptoBarMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.control.CryptoSubscriptionsMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.orderbook.CryptoOrderBookMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.quote.CryptoQuoteMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.trade.CryptoTradeMessage; import net.jacobpeterson.alpaca.websocket.marketdata.MarketDataWebsocket; +import okhttp3.HttpUrl; import okhttp3.OkHttpClient; +import java.util.Set; + +import static net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.CryptoMarketDataMessageType.ERROR; +import static net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.CryptoMarketDataMessageType.SUBSCRIPTION; +import static net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.CryptoMarketDataMessageType.SUCCESS; +import static net.jacobpeterson.alpaca.openapi.marketdata.JSON.getGson; + /** - * {@link CryptoMarketDataWebsocket} is a {@link MarketDataWebsocket} for - * Realtime - * Crypto Market Data + * {@link CryptoMarketDataWebsocket} is an implementation for {@link CryptoMarketDataWebsocketInterface}. */ -public class CryptoMarketDataWebsocket extends MarketDataWebsocket { +public class CryptoMarketDataWebsocket + extends MarketDataWebsocket + implements CryptoMarketDataWebsocketInterface { /** * Instantiates a new {@link CryptoMarketDataWebsocket}. * - * @param okHttpClient the {@link OkHttpClient} - * @param keyID the key ID - * @param secretKey the secret key + * @param okHttpClient the {@link OkHttpClient} + * @param traderKeyID the trader key ID + * @param traderSecretKey the trader secret key + * @param brokerAPIKey the broker API key + * @param brokerAPISecret the broker API secret */ - public CryptoMarketDataWebsocket(OkHttpClient okHttpClient, String keyID, String secretKey) { - super(okHttpClient, "v1beta3/crypto/us", "Crypto", keyID, secretKey, CryptoTradeMessage.class, - CryptoQuoteMessage.class, CryptoBarMessage.class); + public CryptoMarketDataWebsocket(OkHttpClient okHttpClient, String traderKeyID, String traderSecretKey, + String brokerAPIKey, String brokerAPISecret) { + super(okHttpClient, new HttpUrl.Builder() + .scheme("https") + .host("stream.data.alpaca.markets") + .addPathSegments("v1beta3/crypto/us") + .build(), + "Crypto", traderKeyID, traderSecretKey, brokerAPIKey, brokerAPISecret, + CryptoMarketDataMessageType.class, CryptoSubscriptionsMessage.class); + } + + @Override + protected boolean isSuccessMessageType(CryptoMarketDataMessageType messageType) { + return messageType == SUCCESS; + } + + @Override + protected boolean isErrorMessageType(CryptoMarketDataMessageType messageType) { + return messageType == ERROR; + } + + @Override + protected boolean isSubscriptionMessageType(CryptoMarketDataMessageType messageType) { + return messageType == SUBSCRIPTION; + } + + @Override + protected void callListenerWithMessage(CryptoMarketDataMessageType messageType, JsonObject messageObject) { + switch (messageType) { + case TRADES: + listener.onTrade(getGson().fromJson(messageObject, CryptoTradeMessage.class)); + break; + case QUOTES: + listener.onQuote(getGson().fromJson(messageObject, CryptoQuoteMessage.class)); + break; + case MINUTE_BARS: + listener.onMinuteBar(getGson().fromJson(messageObject, CryptoBarMessage.class)); + break; + case DAILY_BARS: + listener.onDailyBar(getGson().fromJson(messageObject, CryptoBarMessage.class)); + break; + case UPDATED_BARS: + listener.onUpdatedBar(getGson().fromJson(messageObject, CryptoBarMessage.class)); + break; + case ORDER_BOOKS: + listener.onOrderBook(getGson().fromJson(messageObject, CryptoOrderBookMessage.class)); + break; + default: + throw new UnsupportedOperationException(); + } + } + + @Override + public void setListener(CryptoMarketDataListener listener) { + this.listener = listener; + } + + @Override + public void setTradeSubscriptions(Set symbols) { + symbols = symbols == null ? Set.of() : symbols; + setSubscriptions(getTradeSubscriptions(), symbols, + set -> newEmptyCryptoSubscriptionsMessage().withTrades(set)); + } + + @Override + public Set getTradeSubscriptions() { + return subscriptionsMessage == null ? Set.of() : subscriptionsMessage.getTrades(); + } + + @Override + public void setQuoteSubscriptions(Set symbols) { + symbols = symbols == null ? Set.of() : symbols; + setSubscriptions(getQuoteSubscriptions(), symbols, + set -> newEmptyCryptoSubscriptionsMessage().withQuotes(set)); + } + + @Override + public Set getQuoteSubscriptions() { + return subscriptionsMessage == null ? Set.of() : subscriptionsMessage.getQuotes(); + } + + @Override + public void setMinuteBarSubscriptions(Set symbols) { + symbols = symbols == null ? Set.of() : symbols; + setSubscriptions(getMinuteBarSubscriptions(), symbols, + set -> newEmptyCryptoSubscriptionsMessage().withMinuteBars(set)); + } + + @Override + public Set getMinuteBarSubscriptions() { + return subscriptionsMessage == null ? Set.of() : subscriptionsMessage.getMinuteBars(); + } + + @Override + public void setDailyBarSubscriptions(Set symbols) { + symbols = symbols == null ? Set.of() : symbols; + setSubscriptions(getDailyBarSubscriptions(), symbols, + set -> newEmptyCryptoSubscriptionsMessage().withDailyBars(set)); + } + + @Override + public Set getDailyBarSubscriptions() { + return subscriptionsMessage == null ? Set.of() : subscriptionsMessage.getDailyBars(); + } + + @Override + public void setUpdatedBarSubscriptions(Set symbols) { + symbols = symbols == null ? Set.of() : symbols; + setSubscriptions(getUpdatedBarSubscriptions(), symbols, + set -> newEmptyCryptoSubscriptionsMessage().withUpdatedBars(set)); + } + + @Override + public Set getUpdatedBarSubscriptions() { + return subscriptionsMessage == null ? Set.of() : subscriptionsMessage.getUpdatedBars(); + } + + @Override + public void setOrderBookSubscriptions(Set symbols) { + symbols = symbols == null ? Set.of() : symbols; + setSubscriptions(getOrderBookSubscriptions(), symbols, + set -> newEmptyCryptoSubscriptionsMessage().withOrderBooks(set)); + } + + @Override + public Set getOrderBookSubscriptions() { + return subscriptionsMessage == null ? Set.of() : subscriptionsMessage.getOrderBooks(); + } + + private CryptoSubscriptionsMessage newEmptyCryptoSubscriptionsMessage() { + return new CryptoSubscriptionsMessage() + .withTrades(null) + .withQuotes(null) + .withMinuteBars(null) + .withDailyBars(null) + .withUpdatedBars(null) + .withOrderBooks(null); } } diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/CryptoMarketDataWebsocketInterface.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/CryptoMarketDataWebsocketInterface.java new file mode 100644 index 00000000..a24e250f --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/CryptoMarketDataWebsocketInterface.java @@ -0,0 +1,116 @@ +package net.jacobpeterson.alpaca.websocket.marketdata.streams.crypto; + +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.CryptoMarketDataMessageType; +import net.jacobpeterson.alpaca.websocket.marketdata.MarketDataWebsocketInterface; + +import java.util.Set; + +/** + * {@link CryptoMarketDataWebsocketInterface} is a {@link MarketDataWebsocketInterface} for + * {@link CryptoMarketDataWebsocket}. + */ +public interface CryptoMarketDataWebsocketInterface extends MarketDataWebsocketInterface { + + /** + * Sets the {@link CryptoMarketDataListener}. + * + * @param listener the {@link CryptoMarketDataListener} + */ + void setListener(CryptoMarketDataListener listener); + + /** + * Subscribes the given symbols to {@link CryptoMarketDataMessageType#TRADES}. This will remove all + * previous {@link CryptoMarketDataMessageType#QUOTES} subscriptions. + * + * @param symbols the {@link Set} of symbols (use an asterisk ("*") as a wildcard to subscribe to all available + * symbols) or null to unsubscribe from all symbols + */ + void setTradeSubscriptions(Set symbols); + + /** + * Gets the current symbols subscribed to {@link CryptoMarketDataMessageType#TRADES}. + * + * @return a {@link Set} of {@link String} symbols + */ + Set getTradeSubscriptions(); + + /** + * Subscribes the given symbols to {@link CryptoMarketDataMessageType#QUOTES}. This will remove all + * previous {@link CryptoMarketDataMessageType#QUOTES} subscriptions. + * + * @param symbols the {@link Set} of symbols (use an asterisk ("*") as a wildcard to subscribe to all available + * symbols) or null to unsubscribe from all symbols + */ + void setQuoteSubscriptions(Set symbols); + + /** + * Gets the current symbols subscribed to {@link CryptoMarketDataMessageType#QUOTES}. + * + * @return a {@link Set} of {@link String} symbols + */ + Set getQuoteSubscriptions(); + + /** + * Subscribes the given symbols to {@link CryptoMarketDataMessageType#MINUTE_BARS}. This will remove + * all previous {@link CryptoMarketDataMessageType#QUOTES} subscriptions. + * + * @param symbols the {@link Set} of symbols (use an asterisk ("*") as a wildcard to subscribe to all available + * symbols) or null to unsubscribe from all symbols + */ + void setMinuteBarSubscriptions(Set symbols); + + /** + * Gets the current symbols subscribed to {@link CryptoMarketDataMessageType#MINUTE_BARS}. + * + * @return a {@link Set} of {@link String} symbols + */ + Set getMinuteBarSubscriptions(); + + /** + * Subscribes the given symbols to {@link CryptoMarketDataMessageType#DAILY_BARS}. This will remove all + * previous {@link CryptoMarketDataMessageType#QUOTES} subscriptions. + * + * @param symbols the {@link Set} of symbols (use an asterisk ("*") as a wildcard to subscribe to all available + * symbols) or null to unsubscribe from all symbols + */ + void setDailyBarSubscriptions(Set symbols); + + /** + * Gets the current symbols subscribed to {@link CryptoMarketDataMessageType#DAILY_BARS}. + * + * @return a {@link Set} of {@link String} symbols + */ + Set getDailyBarSubscriptions(); + + /** + * Subscribes the given symbols to {@link CryptoMarketDataMessageType#UPDATED_BARS}. This will remove + * all previous {@link CryptoMarketDataMessageType#QUOTES} subscriptions. + * + * @param symbols the {@link Set} of symbols (use an asterisk ("*") as a wildcard to subscribe to all available + * symbols) or null to unsubscribe from all symbols + */ + void setUpdatedBarSubscriptions(Set symbols); + + /** + * Gets the current symbols subscribed to {@link CryptoMarketDataMessageType#UPDATED_BARS}. + * + * @return a {@link Set} of {@link String} symbols + */ + Set getUpdatedBarSubscriptions(); + + /** + * Subscribes the given symbols to {@link CryptoMarketDataMessageType#ORDER_BOOKS}. This will remove + * all previous {@link CryptoMarketDataMessageType#QUOTES} subscriptions. + * + * @param symbols the {@link Set} of symbols (use an asterisk ("*") as a wildcard to subscribe to all available + * symbols) or null to unsubscribe from all symbols + */ + void setOrderBookSubscriptions(Set symbols); + + /** + * Gets the current symbols subscribed to {@link CryptoMarketDataMessageType#ORDER_BOOKS}. + * + * @return a {@link Set} of {@link String} symbols + */ + Set getOrderBookSubscriptions(); +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/control/crypto_subscriptions_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/control/crypto_subscriptions_message.json new file mode 100644 index 00000000..6d8438a8 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/control/crypto_subscriptions_message.json @@ -0,0 +1,34 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.CryptoMarketDataMessage" + }, + "properties": { + "trades": { + "existingJavaType": "java.util.Set", + "title": "The {@link java.util.Set} of symbols subscribed to trades." + }, + "quotes": { + "existingJavaType": "java.util.Set", + "title": "The {@link java.util.Set} of symbols subscribed to quotes." + }, + "bars": { + "existingJavaType": "java.util.Set", + "javaName": "minuteBars", + "title": "The {@link java.util.Set} of symbols subscribed to minute bars." + }, + "dailyBars": { + "existingJavaType": "java.util.Set", + "title": "The {@link java.util.Set} of symbols subscribed to daily bars." + }, + "updatedBars": { + "existingJavaType": "java.util.Set", + "title": "The {@link java.util.Set} of symbols subscribed to updated bars." + }, + "orderbooks": { + "existingJavaType": "java.util.Set", + "javaName": "orderBooks", + "title": "The {@link java.util.Set} of symbols subscribed to order books." + } + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/orderbook/crypto_orderbook_entry.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/orderbook/crypto_order_book_entry.json similarity index 100% rename from src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/orderbook/crypto_orderbook_entry.json rename to src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/orderbook/crypto_order_book_entry.json diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/orderbook/crypto_order_book_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/orderbook/crypto_order_book_message.json new file mode 100644 index 00000000..76c632fa --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/orderbook/crypto_order_book_message.json @@ -0,0 +1,28 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.CryptoMarketDataMessage" + }, + "properties": { + "S": { + "existingJavaType": "java.lang.String", + "javaName": "symbol", + "title": "The symbol." + }, + "a": { + "existingJavaType": "java.util.Set", + "javaName": "asks", + "title": "The {@link java.util.Set} of {@link net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.orderbook.CryptoOrderBookEntry} asks." + }, + "b": { + "existingJavaType": "java.util.Set", + "javaName": "bids", + "title": "The {@link java.util.Set} of {@link net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.orderbook.CryptoOrderBookEntry} bids." + }, + "t": { + "existingJavaType": "java.time.OffsetDateTime", + "javaName": "timestamp", + "title": "The timestamp with nanosecond precision." + } + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/orderbook/crypto_orderbook_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/orderbook/crypto_orderbook_message.json deleted file mode 100644 index 80aa0c54..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/orderbook/crypto_orderbook_message.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "type": "object", - "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.CryptoMarketDataMessage" - }, - "properties": { - "S": { - "existingJavaType": "java.lang.String", - "javaName": "symbol", - "title": "The symbol." - }, - "a": { - "existingJavaType": "java.util.ArrayList", - "javaName": "asks", - "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.orderbook.CryptoOrderBookEntry} asks." - }, - "b": { - "existingJavaType": "java.util.ArrayList", - "javaName": "bids", - "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.orderbook.CryptoOrderBookEntry} bids." - }, - "t": { - "existingJavaType": "java.time.OffsetDateTime", - "javaName": "timestamp", - "title": "The timestamp with nanosecond precision." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/subscriptions/crypto_subscriptions_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/subscriptions/crypto_subscriptions_message.json deleted file mode 100644 index 19b1f191..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/subscriptions/crypto_subscriptions_message.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "type": "object", - "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.CryptoMarketDataMessage" - }, - "properties": { - "trades": { - "existingJavaType": "java.util.ArrayList", - "title": "The {@link java.util.ArrayList} of symbols subscribed to trades." - }, - "quotes": { - "existingJavaType": "java.util.ArrayList", - "title": "The {@link java.util.ArrayList} of symbols subscribed to quotes." - }, - "bars": { - "existingJavaType": "java.util.ArrayList", - "javaName": "minuteBars", - "title": "The {@link java.util.ArrayList} of symbols subscribed to minute bars." - }, - "dailyBars": { - "existingJavaType": "java.util.ArrayList", - "title": "The {@link java.util.ArrayList} of symbols subscribed to daily bars." - }, - "updatedBars": { - "existingJavaType": "java.util.ArrayList", - "title": "The {@link java.util.ArrayList} of symbols subscribed to updated bars." - }, - "orderbooks": { - "existingJavaType": "java.util.ArrayList", - "title": "The {@link java.util.ArrayList} of symbols subscribed to orderbooks." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/NewsMarketDataListener.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/NewsMarketDataListener.java new file mode 100644 index 00000000..c47cb6dc --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/NewsMarketDataListener.java @@ -0,0 +1,17 @@ +package net.jacobpeterson.alpaca.websocket.marketdata.streams.news; + +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.news.model.news.NewsMessage; + +/** + * {@link NewsMarketDataListener} defines a listener interface for {@link NewsMarketDataWebsocketInterface} messages. + */ +@FunctionalInterface +public interface NewsMarketDataListener { + + /** + * Called when a {@link NewsMessage} is received. + * + * @param news the {@link NewsMessage} + */ + void onNews(NewsMessage news); +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/NewsMarketDataListenerAdapter.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/NewsMarketDataListenerAdapter.java new file mode 100644 index 00000000..2278df82 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/NewsMarketDataListenerAdapter.java @@ -0,0 +1,12 @@ +package net.jacobpeterson.alpaca.websocket.marketdata.streams.news; + +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.news.model.news.NewsMessage; + +/** + * {@link NewsMarketDataListenerAdapter} is an adapter for {@link NewsMarketDataListener}. + */ +public class NewsMarketDataListenerAdapter implements NewsMarketDataListener { + + @Override + public void onNews(NewsMessage news) {} +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/NewsMarketDataWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/NewsMarketDataWebsocket.java index 8e92fa69..495f7932 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/NewsMarketDataWebsocket.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/NewsMarketDataWebsocket.java @@ -1,15 +1,85 @@ package net.jacobpeterson.alpaca.websocket.marketdata.streams.news; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.bar.BarMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.quote.QuoteMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.common.realtime.trade.TradeMessage; +import com.google.gson.JsonObject; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.news.model.NewsMarketDataMessageType; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.news.model.control.NewsSubscriptionsMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.news.model.news.NewsMessage; import net.jacobpeterson.alpaca.websocket.marketdata.MarketDataWebsocket; +import okhttp3.HttpUrl; import okhttp3.OkHttpClient; -public class NewsMarketDataWebsocket extends MarketDataWebsocket { - public NewsMarketDataWebsocket(OkHttpClient okHttpClient, - String keyID, String secretKey) { - super(okHttpClient, "v1beta1/news", "News", keyID, secretKey, TradeMessage.class, - QuoteMessage.class, BarMessage.class); +import java.util.Set; + +import static net.jacobpeterson.alpaca.model.websocket.marketdata.streams.news.model.NewsMarketDataMessageType.ERROR; +import static net.jacobpeterson.alpaca.model.websocket.marketdata.streams.news.model.NewsMarketDataMessageType.SUBSCRIPTION; +import static net.jacobpeterson.alpaca.model.websocket.marketdata.streams.news.model.NewsMarketDataMessageType.SUCCESS; +import static net.jacobpeterson.alpaca.openapi.marketdata.JSON.getGson; + +/** + * {@link NewsMarketDataWebsocket} is an implementation for {@link NewsMarketDataWebsocketInterface}. + */ +public class NewsMarketDataWebsocket + extends MarketDataWebsocket + implements NewsMarketDataWebsocketInterface { + + /** + * Instantiates a new {@link NewsMarketDataWebsocket}. + * + * @param okHttpClient the {@link OkHttpClient} + * @param traderKeyID the trader key ID + * @param traderSecretKey the trader secret key + * @param brokerAPIKey the broker API key + * @param brokerAPISecret the broker API secret + */ + public NewsMarketDataWebsocket(OkHttpClient okHttpClient, String traderKeyID, String traderSecretKey, + String brokerAPIKey, String brokerAPISecret) { + super(okHttpClient, new HttpUrl.Builder() + .scheme("https") + .host("stream.data.alpaca.markets") + .addPathSegments("v1beta1/news") + .build(), + "News", traderKeyID, traderSecretKey, brokerAPIKey, brokerAPISecret, + NewsMarketDataMessageType.class, NewsSubscriptionsMessage.class); + } + + @Override + protected boolean isSuccessMessageType(NewsMarketDataMessageType messageType) { + return messageType == SUCCESS; + } + + @Override + protected boolean isErrorMessageType(NewsMarketDataMessageType messageType) { + return messageType == ERROR; + } + + @Override + protected boolean isSubscriptionMessageType(NewsMarketDataMessageType messageType) { + return messageType == SUBSCRIPTION; + } + + @Override + protected void callListenerWithMessage(NewsMarketDataMessageType messageType, JsonObject messageObject) { + if (messageType == NewsMarketDataMessageType.NEWS) { + listener.onNews(getGson().fromJson(messageObject, NewsMessage.class)); + } else { + throw new UnsupportedOperationException(); + } + } + + @Override + public void setListener(NewsMarketDataListener listener) { + this.listener = listener; + } + + @Override + public void setNewsSubscriptions(Set symbols) { + symbols = symbols == null ? Set.of() : symbols; + setSubscriptions(getNewsSubscriptions(), symbols, + set -> new NewsSubscriptionsMessage().withNews(set)); + } + + @Override + public Set getNewsSubscriptions() { + return subscriptionsMessage == null ? Set.of() : subscriptionsMessage.getNews(); } } diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/NewsMarketDataWebsocketInterface.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/NewsMarketDataWebsocketInterface.java new file mode 100644 index 00000000..8a89857d --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/NewsMarketDataWebsocketInterface.java @@ -0,0 +1,36 @@ +package net.jacobpeterson.alpaca.websocket.marketdata.streams.news; + +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.news.model.NewsMarketDataMessageType; +import net.jacobpeterson.alpaca.websocket.marketdata.MarketDataWebsocketInterface; + +import java.util.Set; + +/** + * {@link NewsMarketDataWebsocketInterface} is a {@link MarketDataWebsocketInterface} for + * {@link NewsMarketDataWebsocket}. + */ +public interface NewsMarketDataWebsocketInterface extends MarketDataWebsocketInterface { + + /** + * Sets the {@link NewsMarketDataListener}. + * + * @param listener the {@link NewsMarketDataListener} + */ + void setListener(NewsMarketDataListener listener); + + /** + * Subscribes the given symbols to {@link NewsMarketDataMessageType#NEWS}. This will remove all + * previous {@link NewsMarketDataMessageType#NEWS} subscriptions. + * + * @param symbols the {@link Set} of symbols (use an asterisk ("*") as a wildcard to subscribe to all available + * symbols) or null to unsubscribe from all symbols + */ + void setNewsSubscriptions(Set symbols); + + /** + * Gets the current symbols subscribed to {@link NewsMarketDataMessageType#NEWS}. + * + * @return a {@link Set} of {@link String} symbols + */ + Set getNewsSubscriptions(); +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/model/control/news_subscriptions_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/model/control/news_subscriptions_message.json new file mode 100644 index 00000000..dbd84da7 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/model/control/news_subscriptions_message.json @@ -0,0 +1,12 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.streams.news.model.NewsMarketDataMessage" + }, + "properties": { + "news": { + "existingJavaType": "java.util.Set", + "title": "The {@link java.util.Set} of symbols subscribed to news." + } + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/model/news/news_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/model/news/news_message.json index 047c0790..638a3a67 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/model/news/news_message.json +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/model/news/news_message.json @@ -26,7 +26,7 @@ "existingJavaType": "java.lang.String" }, "symbols": { - "existingJavaType": "java.util.ArrayList" + "existingJavaType": "java.util.Set" }, "created_at": { "existingJavaType": "java.time.OffsetDateTime" diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/StockMarketDataListener.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/StockMarketDataListener.java new file mode 100644 index 00000000..396e6af2 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/StockMarketDataListener.java @@ -0,0 +1,78 @@ +package net.jacobpeterson.alpaca.websocket.marketdata.streams.stock; + +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.bar.StockBarMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.limituplimitdownband.StockLimitUpLimitDownBandMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.quote.StockQuoteMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.trade.StockTradeMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.tradecancelerror.StockTradeCancelErrorMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.tradecorrection.StockTradeCorrectionMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.tradingstatus.StockTradingStatusMessage; + +/** + * {@link StockMarketDataListener} defines a listener interface for {@link StockMarketDataWebsocketInterface} messages. + */ +public interface StockMarketDataListener { + + /** + * Called when a {@link StockTradeMessage} is received. + * + * @param trade the {@link StockTradeMessage} + */ + void onTrade(StockTradeMessage trade); + + /** + * Called when a {@link StockQuoteMessage} is received. + * + * @param quote the {@link StockQuoteMessage} + */ + void onQuote(StockQuoteMessage quote); + + /** + * Called when a {@link StockBarMessage} is received. + * + * @param bar the {@link StockBarMessage} + */ + void onMinuteBar(StockBarMessage bar); + + /** + * Called when a {@link StockBarMessage} is received. + * + * @param bar the {@link StockBarMessage} + */ + void onDailyBar(StockBarMessage bar); + + /** + * Called when a {@link StockBarMessage} is received. + * + * @param bar the {@link StockBarMessage} + */ + void onUpdatedBar(StockBarMessage bar); + + /** + * Called when a {@link StockTradeCorrectionMessage} is received. + * + * @param tradeCorrection the {@link StockTradeCorrectionMessage} + */ + void onTradeCorrection(StockTradeCorrectionMessage tradeCorrection); + + /** + * Called when a {@link StockTradeCancelErrorMessage} is received. + * + * @param tradeCancelError the {@link StockTradeCancelErrorMessage} + */ + void onTradeCancelError(StockTradeCancelErrorMessage tradeCancelError); + + /** + * Called when a {@link StockLimitUpLimitDownBandMessage} is received. + * + * @param limitUpLimitDownBand the {@link StockLimitUpLimitDownBandMessage} + */ + void onLimitUpLimitDownBand(StockLimitUpLimitDownBandMessage limitUpLimitDownBand); + + /** + * Called when a {link StockTradingStatusMessage} is received. + * + * @param tradingStatus the {@link StockTradingStatusMessage} + */ + void onTradingStatus(StockTradingStatusMessage tradingStatus); +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/StockMarketDataListenerAdapter.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/StockMarketDataListenerAdapter.java new file mode 100644 index 00000000..df99badf --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/StockMarketDataListenerAdapter.java @@ -0,0 +1,42 @@ +package net.jacobpeterson.alpaca.websocket.marketdata.streams.stock; + +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.bar.StockBarMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.limituplimitdownband.StockLimitUpLimitDownBandMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.quote.StockQuoteMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.trade.StockTradeMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.tradecancelerror.StockTradeCancelErrorMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.tradecorrection.StockTradeCorrectionMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.tradingstatus.StockTradingStatusMessage; + +/** + * {@link StockMarketDataListenerAdapter} is an adapter for {@link StockMarketDataListener}. + */ +public class StockMarketDataListenerAdapter implements StockMarketDataListener { + + @Override + public void onTrade(StockTradeMessage trade) {} + + @Override + public void onQuote(StockQuoteMessage quote) {} + + @Override + public void onMinuteBar(StockBarMessage bar) {} + + @Override + public void onDailyBar(StockBarMessage bar) {} + + @Override + public void onUpdatedBar(StockBarMessage bar) {} + + @Override + public void onTradeCorrection(StockTradeCorrectionMessage tradeCorrection) {} + + @Override + public void onTradeCancelError(StockTradeCancelErrorMessage tradeCancelError) {} + + @Override + public void onLimitUpLimitDownBand(StockLimitUpLimitDownBandMessage limitUpLimitDownBand) {} + + @Override + public void onTradingStatus(StockTradingStatusMessage tradingStatus) {} +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/StockMarketDataWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/StockMarketDataWebsocket.java index 87bfe6c9..753d0246 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/StockMarketDataWebsocket.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/StockMarketDataWebsocket.java @@ -1,30 +1,218 @@ package net.jacobpeterson.alpaca.websocket.marketdata.streams.stock; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.bar.StockBarMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.quote.StockQuoteMessage; -import net.jacobpeterson.alpaca.model.endpoint.marketdata.stock.realtime.trade.StockTradeMessage; -import net.jacobpeterson.alpaca.model.properties.DataAPIType; +import com.google.gson.JsonObject; +import net.jacobpeterson.alpaca.model.util.apitype.MarketDataWebsocketSourceType; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.StockMarketDataMessageType; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.bar.StockBarMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.control.StockSubscriptionsMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.limituplimitdownband.StockLimitUpLimitDownBandMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.quote.StockQuoteMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.trade.StockTradeMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.tradecancelerror.StockTradeCancelErrorMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.tradecorrection.StockTradeCorrectionMessage; +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.tradingstatus.StockTradingStatusMessage; import net.jacobpeterson.alpaca.websocket.marketdata.MarketDataWebsocket; +import okhttp3.HttpUrl; import okhttp3.OkHttpClient; +import java.util.Set; + +import static net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.StockMarketDataMessageType.ERROR; +import static net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.StockMarketDataMessageType.SUBSCRIPTION; +import static net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.StockMarketDataMessageType.SUCCESS; +import static net.jacobpeterson.alpaca.openapi.marketdata.JSON.getGson; + /** - * {@link StockMarketDataWebsocket} is a {@link MarketDataWebsocket} for - * Realtime - * Stock Market Data + * {@link StockMarketDataWebsocket} is an implementation for {@link StockMarketDataWebsocketInterface}. */ -public class StockMarketDataWebsocket extends MarketDataWebsocket { +public class StockMarketDataWebsocket + extends MarketDataWebsocket + implements StockMarketDataWebsocketInterface { + + private static HttpUrl createWebsocketURL(boolean isSandbox, + MarketDataWebsocketSourceType marketDataWebsocketSourceType) { + return new HttpUrl.Builder() + .scheme("https") + .host(isSandbox ? "stream.data.sandbox.alpaca.markets" : "stream.data.alpaca.markets") + .addPathSegment("v2") + .addPathSegment(marketDataWebsocketSourceType.toString()) + .build(); + } /** * Instantiates a new {@link StockMarketDataWebsocket}. * - * @param okHttpClient the {@link OkHttpClient} - * @param dataAPIType the {@link DataAPIType} - * @param keyID the key ID - * @param secretKey the secret key + * @param okHttpClient the {@link OkHttpClient} + * @param traderKeyID the trader key ID + * @param traderSecretKey the trader secret key + * @param brokerAPIKey the broker API key + * @param brokerAPISecret the broker API secret + * @param marketDataWebsocketSourceType the {@link MarketDataWebsocketSourceType} */ - public StockMarketDataWebsocket(OkHttpClient okHttpClient, DataAPIType dataAPIType, - String keyID, String secretKey) { - super(okHttpClient, "v2/" + dataAPIType.toString(), "Stock", keyID, secretKey, StockTradeMessage.class, - StockQuoteMessage.class, StockBarMessage.class); + public StockMarketDataWebsocket(OkHttpClient okHttpClient, String traderKeyID, String traderSecretKey, + String brokerAPIKey, String brokerAPISecret, MarketDataWebsocketSourceType marketDataWebsocketSourceType) { + super(okHttpClient, + createWebsocketURL(brokerAPIKey != null && brokerAPISecret != null, marketDataWebsocketSourceType), + "Stock", traderKeyID, traderSecretKey, brokerAPIKey, brokerAPISecret, + StockMarketDataMessageType.class, StockSubscriptionsMessage.class); + } + + @Override + protected boolean isSuccessMessageType(StockMarketDataMessageType messageType) { + return messageType == SUCCESS; + } + + @Override + protected boolean isErrorMessageType(StockMarketDataMessageType messageType) { + return messageType == ERROR; + } + + @Override + protected boolean isSubscriptionMessageType(StockMarketDataMessageType messageType) { + return messageType == SUBSCRIPTION; + } + + @Override + protected void callListenerWithMessage(StockMarketDataMessageType messageType, JsonObject messageObject) { + switch (messageType) { + case TRADES: + listener.onTrade(getGson().fromJson(messageObject, + StockTradeMessage.class)); + break; + case QUOTES: + listener.onQuote(getGson().fromJson(messageObject, + StockQuoteMessage.class)); + break; + case MINUTE_BARS: + listener.onMinuteBar(getGson().fromJson(messageObject, + StockBarMessage.class)); + break; + case DAILY_BARS: + listener.onDailyBar(getGson().fromJson(messageObject, + StockBarMessage.class)); + break; + case UPDATED_BARS: + listener.onUpdatedBar(getGson().fromJson(messageObject, + StockBarMessage.class)); + break; + case TRADE_CORRECTIONS: + listener.onTradeCorrection(getGson().fromJson(messageObject, + StockTradeCorrectionMessage.class)); + break; + case TRADE_CANCEL_ERRORS: + listener.onTradeCancelError(getGson().fromJson(messageObject, + StockTradeCancelErrorMessage.class)); + break; + case LIMIT_UP_LIMIT_DOWN_BANDS: + listener.onLimitUpLimitDownBand(getGson().fromJson(messageObject, + StockLimitUpLimitDownBandMessage.class)); + break; + case TRADING_STATUSES: + listener.onTradingStatus(getGson().fromJson(messageObject, + StockTradingStatusMessage.class)); + break; + default: + throw new UnsupportedOperationException(); + } + } + + @Override + public void setListener(StockMarketDataListener listener) { + this.listener = listener; + } + + @Override + public void setTradeSubscriptions(Set symbols) { + symbols = symbols == null ? Set.of() : symbols; + setSubscriptions(getTradeSubscriptions(), symbols, + set -> newEmptyStockSubscriptionsMessage().withTrades(set)); + } + + @Override + public Set getTradeSubscriptions() { + return subscriptionsMessage == null ? Set.of() : subscriptionsMessage.getTrades(); + } + + @Override + public void setQuoteSubscriptions(Set symbols) { + symbols = symbols == null ? Set.of() : symbols; + setSubscriptions(getQuoteSubscriptions(), symbols, + set -> newEmptyStockSubscriptionsMessage().withQuotes(set)); + } + + @Override + public Set getQuoteSubscriptions() { + return subscriptionsMessage == null ? Set.of() : subscriptionsMessage.getQuotes(); + } + + @Override + public void setMinuteBarSubscriptions(Set symbols) { + symbols = symbols == null ? Set.of() : symbols; + setSubscriptions(getMinuteBarSubscriptions(), symbols, + set -> newEmptyStockSubscriptionsMessage().withMinuteBars(set)); + } + + @Override + public Set getMinuteBarSubscriptions() { + return subscriptionsMessage == null ? Set.of() : subscriptionsMessage.getMinuteBars(); + } + + @Override + public void setDailyBarSubscriptions(Set symbols) { + symbols = symbols == null ? Set.of() : symbols; + setSubscriptions(getDailyBarSubscriptions(), symbols, + set -> newEmptyStockSubscriptionsMessage().withDailyBars(set)); + } + + @Override + public Set getDailyBarSubscriptions() { + return subscriptionsMessage == null ? Set.of() : subscriptionsMessage.getDailyBars(); + } + + @Override + public void setUpdatedBarSubscriptions(Set symbols) { + symbols = symbols == null ? Set.of() : symbols; + setSubscriptions(getUpdatedBarSubscriptions(), symbols, + set -> newEmptyStockSubscriptionsMessage().withUpdatedBars(set)); + } + + @Override + public Set getUpdatedBarSubscriptions() { + return subscriptionsMessage == null ? Set.of() : subscriptionsMessage.getUpdatedBars(); + } + + @Override + public void setLimitUpLimitDownBandSubscriptions(Set symbols) { + symbols = symbols == null ? Set.of() : symbols; + setSubscriptions(getLimitUpLimitDownBandSubscriptions(), symbols, + set -> newEmptyStockSubscriptionsMessage().withLimitUpLimitDownBands(set)); + } + + @Override + public Set getLimitUpLimitDownBandSubscriptions() { + return subscriptionsMessage == null ? Set.of() : subscriptionsMessage.getLimitUpLimitDownBands(); + } + + @Override + public void setTradingStatuseSubscriptions(Set symbols) { + symbols = symbols == null ? Set.of() : symbols; + setSubscriptions(getTradingStatuseSubscriptions(), symbols, + set -> newEmptyStockSubscriptionsMessage().withTradingStatuses(set)); + } + + @Override + public Set getTradingStatuseSubscriptions() { + return subscriptionsMessage == null ? Set.of() : subscriptionsMessage.getTradingStatuses(); + } + + private StockSubscriptionsMessage newEmptyStockSubscriptionsMessage() { + return new StockSubscriptionsMessage() + .withTrades(null) + .withQuotes(null) + .withMinuteBars(null) + .withDailyBars(null) + .withUpdatedBars(null) + .withLimitUpLimitDownBands(null) + .withTradingStatuses(null); } } diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/StockMarketDataWebsocketInterface.java b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/StockMarketDataWebsocketInterface.java new file mode 100644 index 00000000..32fc3b8d --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/StockMarketDataWebsocketInterface.java @@ -0,0 +1,136 @@ +package net.jacobpeterson.alpaca.websocket.marketdata.streams.stock; + +import net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.StockMarketDataMessageType; +import net.jacobpeterson.alpaca.websocket.marketdata.MarketDataWebsocketInterface; + +import java.util.Set; + +/** + * {@link StockMarketDataWebsocketInterface} is a {@link MarketDataWebsocketInterface} for + * {@link StockMarketDataWebsocket}. + */ +public interface StockMarketDataWebsocketInterface extends MarketDataWebsocketInterface { + + /** + * Sets the {@link StockMarketDataListener}. + * + * @param listener the {@link StockMarketDataListener} + */ + void setListener(StockMarketDataListener listener); + + /** + * Subscribes the given symbols to {@link StockMarketDataMessageType#TRADES}. This will remove all + * previous {@link StockMarketDataMessageType#TRADES} subscriptions. + *
+ * Note that this will automatically apply the same symbols subscription list to the + * {@link StockMarketDataMessageType#TRADE_CORRECTIONS} and {@link StockMarketDataMessageType#TRADE_CANCEL_ERRORS} + * types. + * + * @param symbols the {@link Set} of symbols (use an asterisk ("*") as a wildcard to subscribe to all available + * symbols) or null to unsubscribe from all symbols + */ + void setTradeSubscriptions(Set symbols); + + /** + * Gets the current symbols subscribed to {@link StockMarketDataMessageType#TRADES}. + * + * @return a {@link Set} of {@link String} symbols + */ + Set getTradeSubscriptions(); + + /** + * Subscribes the given symbols to {@link StockMarketDataMessageType#QUOTES}. This will remove all + * previous {@link StockMarketDataMessageType#QUOTES} subscriptions. + * + * @param symbols the {@link Set} of symbols (use an asterisk ("*") as a wildcard to subscribe to all available + * symbols) or null to unsubscribe from all symbols + */ + void setQuoteSubscriptions(Set symbols); + + /** + * Gets the current symbols subscribed to {@link StockMarketDataMessageType#QUOTES}. + * + * @return a {@link Set} of {@link String} symbols + */ + Set getQuoteSubscriptions(); + + /** + * Subscribes the given symbols to {@link StockMarketDataMessageType#MINUTE_BARS}. This will remove all + * previous {@link StockMarketDataMessageType#MINUTE_BARS} subscriptions. + * + * @param symbols the {@link Set} of symbols (use an asterisk ("*") as a wildcard to subscribe to all available + * symbols) or null to unsubscribe from all symbols + */ + void setMinuteBarSubscriptions(Set symbols); + + /** + * Gets the current symbols subscribed to {@link StockMarketDataMessageType#MINUTE_BARS}. + * + * @return a {@link Set} of {@link String} symbols + */ + Set getMinuteBarSubscriptions(); + + /** + * Subscribes the given symbols to {@link StockMarketDataMessageType#DAILY_BARS}. This will remove all + * previous {@link StockMarketDataMessageType#DAILY_BARS} subscriptions. + * + * @param symbols the {@link Set} of symbols (use an asterisk ("*") as a wildcard to subscribe to all available + * symbols) or null to unsubscribe from all symbols + */ + void setDailyBarSubscriptions(Set symbols); + + /** + * Gets the current symbols subscribed to {@link StockMarketDataMessageType#DAILY_BARS}. + * + * @return a {@link Set} of {@link String} symbols + */ + Set getDailyBarSubscriptions(); + + /** + * Subscribes the given symbols to {@link StockMarketDataMessageType#UPDATED_BARS}. This will remove + * all previous {@link StockMarketDataMessageType#UPDATED_BARS} subscriptions. + * + * @param symbols the {@link Set} of symbols (use an asterisk ("*") as a wildcard to subscribe to all available + * symbols) or null to unsubscribe from all symbols + */ + void setUpdatedBarSubscriptions(Set symbols); + + /** + * Gets the current symbols subscribed to {@link StockMarketDataMessageType#UPDATED_BARS}. + * + * @return a {@link Set} of {@link String} symbols + */ + Set getUpdatedBarSubscriptions(); + + /** + * Subscribes the given symbols to {@link StockMarketDataMessageType#LIMIT_UP_LIMIT_DOWN_BANDS}. This + * will remove all previous {@link StockMarketDataMessageType#LIMIT_UP_LIMIT_DOWN_BANDS} subscriptions. + * + * @param symbols the {@link Set} of symbols (use an asterisk ("*") as a wildcard to subscribe to all available + * symbols) or null to unsubscribe from all symbols + */ + void setLimitUpLimitDownBandSubscriptions(Set symbols); + + /** + * Gets the current symbols subscribed to {@link StockMarketDataMessageType#LIMIT_UP_LIMIT_DOWN_BANDS}. + * + * @return a {@link Set} of {@link String} symbols + */ + Set getLimitUpLimitDownBandSubscriptions(); + + /** + * Subscribes the given symbols to {@link StockMarketDataMessageType#TRADING_STATUSES}. This will + * remove all previous {@link StockMarketDataMessageType#TRADING_STATUSES} subscriptions. + * + * @param symbols the {@link Set} of symbols (use an asterisk ("*") as a wildcard to subscribe to all available + * symbols) or null to unsubscribe from all symbols + */ + void setTradingStatuseSubscriptions(Set symbols); + + /** + * Gets the current symbols subscribed to {@link StockMarketDataMessageType#TRADING_STATUSES}. + * + * @return a {@link Set} of {@link String} symbols + */ + Set getTradingStatuseSubscriptions(); +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/control/stock_subscriptions_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/control/stock_subscriptions_message.json new file mode 100644 index 00000000..05a1d449 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/control/stock_subscriptions_message.json @@ -0,0 +1,39 @@ +{ + "type": "object", + "extends": { + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.StockMarketDataMessage" + }, + "properties": { + "trades": { + "existingJavaType": "java.util.Set", + "title": "The {@link java.util.Set} of symbols subscribed to trades." + }, + "quotes": { + "existingJavaType": "java.util.Set", + "title": "The {@link java.util.Set} of symbols subscribed to quotes." + }, + "bars": { + "existingJavaType": "java.util.Set", + "javaName": "minuteBars", + "title": "The {@link java.util.Set} of symbols subscribed to minute bars." + }, + "dailyBars": { + "existingJavaType": "java.util.Set", + "title": "The {@link java.util.Set} of symbols subscribed to daily bars." + }, + "updatedBars": { + "existingJavaType": "java.util.Set", + "title": "The {@link java.util.Set} of symbols subscribed to updated bars." + }, + "lulds": { + "existingJavaType": "java.util.Set", + "javaName": "limitUpLimitDownBands", + "title": "The {@link java.util.Set} of symbols subscribed to limit up - limit down bands." + }, + "statuses": { + "existingJavaType": "java.util.Set", + "javaName": "tradingStatuses", + "title": "The {@link java.util.Set} of symbols subscribed to trading statuses." + } + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/quote/stock_quote_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/quote/stock_quote_message.json index 27ff6daa..b0e2d72f 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/quote/stock_quote_message.json +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/quote/stock_quote_message.json @@ -40,9 +40,9 @@ "title": "The bid size." }, "c": { - "existingJavaType": "java.util.ArrayList", + "existingJavaType": "java.util.Set", "javaName": "conditions", - "title": "The {@link java.util.ArrayList} of quote conditions." + "title": "The {@link java.util.Set} of quote conditions." }, "t": { "existingJavaType": "java.time.OffsetDateTime", diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/subscriptions/stock_subscriptions_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/subscriptions/stock_subscriptions_message.json deleted file mode 100644 index 986947f6..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/subscriptions/stock_subscriptions_message.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "type": "object", - "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.marketdata.streams.stock.model.StockMarketDataMessage" - }, - "properties": { - "trades": { - "existingJavaType": "java.util.ArrayList", - "title": "The {@link java.util.ArrayList} of symbols subscribed to trades." - }, - "quotes": { - "existingJavaType": "java.util.ArrayList", - "title": "The {@link java.util.ArrayList} of symbols subscribed to quotes." - }, - "bars": { - "existingJavaType": "java.util.ArrayList", - "javaName": "minuteBars", - "title": "The {@link java.util.ArrayList} of symbols subscribed to minute bars." - }, - "dailyBars": { - "existingJavaType": "java.util.ArrayList", - "title": "The {@link java.util.ArrayList} of symbols subscribed to daily bars." - }, - "updatedBars": { - "existingJavaType": "java.util.ArrayList", - "title": "The {@link java.util.ArrayList} of symbols subscribed to updated bars." - }, - "corrections": { - "existingJavaType": "java.util.ArrayList", - "javaName": "tradeCorrections", - "title": "The {@link java.util.ArrayList} of symbols subscribed to trade corrections." - }, - "cancelErrors": { - "existingJavaType": "java.util.ArrayList", - "javaName": "tradeCancelErrors", - "title": "The {@link java.util.ArrayList} of symbols subscribed to trade cancels/errors." - }, - "lulds": { - "existingJavaType": "java.util.ArrayList", - "javaName": "limitUpLimitDownBands", - "title": "The {@link java.util.ArrayList} of symbols subscribed to limit up - limit down bands." - }, - "statuses": { - "existingJavaType": "java.util.ArrayList", - "javaName": "tradingStatuses", - "title": "The {@link java.util.ArrayList} of symbols subscribed to trading statuses." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/trade/stock_trade_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/trade/stock_trade_message.json index 4294856a..239a8164 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/trade/stock_trade_message.json +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/trade/stock_trade_message.json @@ -30,9 +30,9 @@ "title": "The trade size." }, "c": { - "existingJavaType": "java.util.ArrayList", + "existingJavaType": "java.util.Set", "javaName": "conditions", - "title": "The {@link java.util.ArrayList} of trade conditions." + "title": "The {@link java.util.Set} of trade conditions." }, "t": { "existingJavaType": "java.time.OffsetDateTime", diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/tradecorrection/stock_trade_correction_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/tradecorrection/stock_trade_correction_message.json index 0b6daaca..acb6a6ab 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/tradecorrection/stock_trade_correction_message.json +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/model/tradecorrection/stock_trade_correction_message.json @@ -30,9 +30,9 @@ "title": "The original trade size." }, "oc": { - "existingJavaType": "java.util.ArrayList", + "existingJavaType": "java.util.Set", "javaName": "originalConditions", - "title": "The {@link java.util.ArrayList} of original trade conditions." + "title": "The {@link java.util.Set} of original trade conditions." }, "ci": { "existingJavaType": "java.lang.Integer", @@ -50,9 +50,9 @@ "title": "The corrected trade size." }, "cc": { - "existingJavaType": "java.util.ArrayList", + "existingJavaType": "java.util.Set", "javaName": "correctedConditions", - "title": "The {@link java.util.ArrayList} of original trade conditions." + "title": "The {@link java.util.Set} of original trade conditions." }, "t": { "existingJavaType": "java.time.OffsetDateTime", diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesListener.java b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesListener.java deleted file mode 100644 index c96baee5..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesListener.java +++ /dev/null @@ -1,11 +0,0 @@ -package net.jacobpeterson.alpaca.websocket.trades; - -import net.jacobpeterson.alpaca.model.websocket.trades.model.TradesStreamMessage; -import net.jacobpeterson.alpaca.model.websocket.trades.model.TradesStreamMessageType; -import net.jacobpeterson.alpaca.websocket.AlpacaWebsocketMessageListener; - -/** - * {@link TradesListener} defines a listener interface for {@link TradesWebsocket} messages. - */ -public interface TradesListener extends AlpacaWebsocketMessageListener { -} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesListenerAdapter.java b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesListenerAdapter.java deleted file mode 100644 index a6dd93ec..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesListenerAdapter.java +++ /dev/null @@ -1,19 +0,0 @@ -package net.jacobpeterson.alpaca.websocket.trades; - -import net.jacobpeterson.alpaca.model.websocket.trades.model.TradesStreamMessage; -import net.jacobpeterson.alpaca.model.websocket.trades.model.TradesStreamMessageType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * {@link TradesListenerAdapter} is an adapter class for {@link TradesListener}. - */ -public class TradesListenerAdapter implements TradesListener { - - private static final Logger LOGGER = LoggerFactory.getLogger(TradesListenerAdapter.class); - - @Override - public void onMessage(TradesStreamMessageType messageType, TradesStreamMessage message) { - LOGGER.info("{} message received: {}", messageType, message); - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesWebsocket.java deleted file mode 100644 index 5759d355..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesWebsocket.java +++ /dev/null @@ -1,217 +0,0 @@ -package net.jacobpeterson.alpaca.websocket.trades; - -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; -import net.jacobpeterson.alpaca.model.util.apitype.TraderAPIEndpointType; -import net.jacobpeterson.alpaca.model.websocket.trades.model.TradesStreamMessage; -import net.jacobpeterson.alpaca.model.websocket.trades.model.TradesStreamMessageType; -import net.jacobpeterson.alpaca.model.websocket.trades.model.authorization.AuthorizationData; -import net.jacobpeterson.alpaca.model.websocket.trades.model.authorization.AuthorizationMessage; -import net.jacobpeterson.alpaca.model.websocket.trades.model.listening.ListeningMessage; -import net.jacobpeterson.alpaca.model.websocket.trades.model.tradeupdate.TradeUpdateMessage; -import net.jacobpeterson.alpaca.websocket.AlpacaWebsocket; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.WebSocket; -import okio.ByteString; -import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; -import static com.google.common.base.Predicates.not; -import static com.google.common.collect.Iterables.toArray; -import static com.google.gson.JsonParser.parseString; -import static java.util.Collections.singletonList; -import static java.util.concurrent.TimeUnit.SECONDS; -import static net.jacobpeterson.alpaca.model.websocket.trades.model.TradesStreamMessageType.TRADE_UPDATES; -import static net.jacobpeterson.alpaca.openapi.trader.JSON.getGson; - -/** - * {@link TradesWebsocket} is an {@link AlpacaWebsocket} implementation and provides the - * {@link TradesWebsocketInterface} interface for - * Trades Streaming. - */ -public class TradesWebsocket extends AlpacaWebsocket - implements TradesWebsocketInterface { - - private static final Logger LOGGER = LoggerFactory.getLogger(TradesWebsocket.class); - private static final String STREAM_ELEMENT_KEY = "stream"; - private static final List SUBSCRIBABLE_STREAMING_MESSAGE_TYPES = - singletonList(TRADE_UPDATES); - - /** - * Creates a {@link HttpUrl} for {@link TradesWebsocket}. - * - * @param traderAPIEndpointType the {@link TraderAPIEndpointType} - * - * @return a {@link HttpUrl} - */ - @SuppressWarnings("UnnecessaryDefault") - private static HttpUrl createWebsocketURL(TraderAPIEndpointType traderAPIEndpointType) { - return new HttpUrl.Builder() - .scheme("https") - .host((switch (traderAPIEndpointType) { - case LIVE -> "api"; - case PAPER -> "paper-api"; - default -> throw new UnsupportedOperationException(); - }) + ".alpaca.markets") - .addPathSegment("stream") - .build(); - } - - private final Set listenedMessageTypes; - - /** - * Instantiates a new {@link TradesWebsocket}. - * - * @param okHttpClient the {@link OkHttpClient} - * @param traderAPIEndpointType the {@link TraderAPIEndpointType} - * @param keyID the key ID - * @param secretKey the secret key - * @param oAuthToken the OAuth token - */ - public TradesWebsocket(OkHttpClient okHttpClient, TraderAPIEndpointType traderAPIEndpointType, - String keyID, String secretKey, String oAuthToken) { - super(okHttpClient, createWebsocketURL(traderAPIEndpointType), "Trades Stream", keyID, secretKey, oAuthToken); - listenedMessageTypes = new HashSet<>(); - } - - @Override - protected void cleanupState() { - super.cleanupState(); - listenedMessageTypes.clear(); - } - - @Override - protected void onConnection() { - sendAuthenticationMessage(); - } - - @Override - protected void onReconnection() { - sendAuthenticationMessage(); - if (waitForAuthorization(5, SECONDS)) { - subscribe(toArray(listenedMessageTypes, TradesStreamMessageType.class)); - } - } - - @Override - protected void sendAuthenticationMessage() { - // Ensure that the authorization Future exists - getAuthorizationFuture(); - - final JsonObject authObject = new JsonObject(); - authObject.addProperty("action", "authenticate"); - final JsonObject authData = new JsonObject(); - if (useOAuth) { - authData.addProperty("oauth_token", oAuthToken); - } else { - authData.addProperty("key_id", keyID); - authData.addProperty("secret_key", secretKey); - } - authObject.add("data", authData); - - LOGGER.info("{} websocket sending authentication message...", websocketName); - websocket.send(authObject.toString()); - } - - @Override - public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString byteString) { // Binary framing - final String messageString = byteString.utf8(); - LOGGER.trace("Websocket message received: {}", messageString); - - // Parse JSON string and identify 'messageType' - final JsonElement messageElement = parseString(messageString); - checkState(messageElement instanceof JsonObject, "Message must be a JsonObject! Received: %s", messageElement); - final JsonObject messageObject = messageElement.getAsJsonObject(); - final JsonElement streamElement = messageObject.get(STREAM_ELEMENT_KEY); - checkState(streamElement instanceof JsonPrimitive, - "Message must contain %s element! Received: %s", STREAM_ELEMENT_KEY, messageElement); - final TradesStreamMessageType messageType = getGson().fromJson(streamElement, TradesStreamMessageType.class); - checkNotNull(messageType, "TradesStreamMessageType not found in message: %s", messageObject); - - // Deserialize message based on 'messageType' - TradesStreamMessage message; - switch (messageType) { - case AUTHORIZATION: - message = getGson().fromJson(messageObject, AuthorizationMessage.class); - final AuthorizationData authorizationData = ((AuthorizationMessage) message).getData(); - authenticated = authorizationData.getAction().equalsIgnoreCase("authenticate") && - authorizationData.getStatus().equalsIgnoreCase("authorized"); - if (!authenticated) { - throw new RuntimeException(websocketName + " websocket authentication failed!"); - } else { - LOGGER.info("{} websocket authenticated.", websocketName); - } - if (authenticationMessageFuture != null) { - authenticationMessageFuture.complete(authenticated); - } - break; - case LISTENING: - message = getGson().fromJson(messageObject, ListeningMessage.class); - // Remove all 'TradeStreamMessageType's that are no longer listened to and add new ones - final List currentTypes = ((ListeningMessage) message).getData().getStreams(); - currentTypes.stream() - .filter(not(listenedMessageTypes::contains)) - .forEach(listenedMessageTypes::remove); - listenedMessageTypes.addAll(currentTypes); - break; - case TRADE_UPDATES: - message = getGson().fromJson(messageObject, TradeUpdateMessage.class); - break; - default: - throw new UnsupportedOperationException(); - } - - // Call listener - if (listenedMessageTypes.contains(messageType)) { - callListener(messageType, message); - } - } - - @Override - public void subscribe(TradesStreamMessageType... messageTypes) { - if (messageTypes == null || messageTypes.length == 0) { - return; - } - - // Add all non-subscribable 'messageTypes' before connecting or sending websocket message - Arrays.stream(messageTypes) - .filter(not(SUBSCRIBABLE_STREAMING_MESSAGE_TYPES::contains)) - .forEach(listenedMessageTypes::add); - - // Create streams subscription JSON message - final JsonObject requestObject = new JsonObject(); - requestObject.addProperty("action", "listen"); - final JsonArray streamsArray = new JsonArray(); - Arrays.stream(messageTypes) - .filter(SUBSCRIBABLE_STREAMING_MESSAGE_TYPES::contains) - .forEach((type) -> streamsArray.add(type.toString())); - if (streamsArray.isEmpty()) { - return; - } else if (!isConnected()) { - throw new IllegalStateException("This websocket must be connected before subscribing to streams!"); - } - final JsonObject dataObject = new JsonObject(); - dataObject.add("streams", streamsArray); - requestObject.add("data", dataObject); - - websocket.send(requestObject.toString()); - LOGGER.info("Requested streams: {}.", streamsArray); - } - - @Override - public Collection subscriptions() { - return new HashSet<>(listenedMessageTypes); - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesWebsocketInterface.java b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesWebsocketInterface.java deleted file mode 100644 index b866e37e..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/TradesWebsocketInterface.java +++ /dev/null @@ -1,26 +0,0 @@ -package net.jacobpeterson.alpaca.websocket.trades; - -import net.jacobpeterson.alpaca.model.websocket.trades.model.TradesStreamMessageType; -import net.jacobpeterson.alpaca.websocket.AlpacaWebsocketInterface; - -import java.util.Collection; - -/** - * {@link TradesWebsocketInterface} is an {@link AlpacaWebsocketInterface} for a {@link TradesWebsocket}. - */ -public interface TradesWebsocketInterface extends AlpacaWebsocketInterface { - - /** - * Sets the {@link TradesStreamMessageType}s for this stream. - * - * @param messageTypes the {@link TradesStreamMessageType}s - */ - void subscribe(TradesStreamMessageType... messageTypes); - - /** - * Gets all the currently subscribed {@link TradesStreamMessageType}s. - * - * @return a {@link Collection} of {@link TradesStreamMessageType} - */ - Collection subscriptions(); -} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/listening/listening_data.json b/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/listening/listening_data.json deleted file mode 100644 index 78a68ba5..00000000 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/listening/listening_data.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "type": "object", - "properties": { - "streams": { - "existingJavaType": "java.util.ArrayList", - "title": "The {@link java.util.ArrayList} of {@link net.jacobpeterson.alpaca.model.websocket.trades.model.TradesStreamMessageType}s." - } - } -} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/updates/UpdatesListener.java b/src/main/java/net/jacobpeterson/alpaca/websocket/updates/UpdatesListener.java new file mode 100644 index 00000000..3a752308 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/updates/UpdatesListener.java @@ -0,0 +1,17 @@ +package net.jacobpeterson.alpaca.websocket.updates; + +import net.jacobpeterson.alpaca.model.websocket.updates.model.tradeupdate.TradeUpdateMessage; + +/** + * {@link UpdatesListener} defines a listener interface for {@link UpdatesWebsocketInterface} messages. + */ +@FunctionalInterface +public interface UpdatesListener { + + /** + * Called when a {@link TradeUpdateMessage} is received. + * + * @param tradeUpdate the {@link TradeUpdateMessage} + */ + void onTradeUpdate(TradeUpdateMessage tradeUpdate); +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/updates/UpdatesWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/updates/UpdatesWebsocket.java new file mode 100644 index 00000000..99e1db42 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/updates/UpdatesWebsocket.java @@ -0,0 +1,172 @@ +package net.jacobpeterson.alpaca.websocket.updates; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import net.jacobpeterson.alpaca.model.util.apitype.TraderAPIEndpointType; +import net.jacobpeterson.alpaca.model.websocket.updates.model.UpdatesMessageType; +import net.jacobpeterson.alpaca.model.websocket.updates.model.authorization.AuthorizationMessage; +import net.jacobpeterson.alpaca.model.websocket.updates.model.tradeupdate.TradeUpdateMessage; +import net.jacobpeterson.alpaca.websocket.AlpacaWebsocket; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.WebSocket; +import okio.ByteString; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.google.gson.JsonParser.parseString; +import static java.util.concurrent.TimeUnit.SECONDS; +import static net.jacobpeterson.alpaca.model.websocket.updates.model.UpdatesMessageType.TRADE_UPDATES; +import static net.jacobpeterson.alpaca.openapi.trader.JSON.getGson; + +/** + * {@link UpdatesWebsocket} is an {@link AlpacaWebsocket} implementation and provides the + * {@link UpdatesWebsocketInterface} interface for + * Updates Streaming. + */ +public class UpdatesWebsocket extends AlpacaWebsocket implements UpdatesWebsocketInterface { + + private static final Logger LOGGER = LoggerFactory.getLogger(UpdatesWebsocket.class); + + @SuppressWarnings("UnnecessaryDefault") + private static HttpUrl createWebsocketURL(TraderAPIEndpointType traderAPIEndpointType) { + return new HttpUrl.Builder() + .scheme("https") + .host((switch (traderAPIEndpointType) { + case LIVE -> "api"; + case PAPER -> "paper-api"; + default -> throw new UnsupportedOperationException(); + }) + ".alpaca.markets") + .addPathSegment("stream") + .build(); + } + + protected final String keyID; + protected final String secretKey; + protected final String oAuthToken; + protected UpdatesListener listener; + protected boolean listenToTradeUpdates; + + /** + * Instantiates a new {@link UpdatesWebsocket}. + * + * @param okHttpClient the {@link OkHttpClient} + * @param traderAPIEndpointType the {@link TraderAPIEndpointType} + * @param keyID the key ID + * @param secretKey the secret key + * @param oAuthToken the OAuth token + */ + public UpdatesWebsocket(OkHttpClient okHttpClient, TraderAPIEndpointType traderAPIEndpointType, + String keyID, String secretKey, String oAuthToken) { + super(okHttpClient, createWebsocketURL(traderAPIEndpointType), "Trades Stream"); + this.keyID = keyID; + this.secretKey = secretKey; + this.oAuthToken = oAuthToken; + } + + @Override + protected void cleanupState() { + super.cleanupState(); + } + + @Override + protected void onConnection() { + sendAuthenticationMessage(); + } + + @Override + protected void onReconnection() { + sendAuthenticationMessage(); + if (waitForAuthorization(5, SECONDS) && listenToTradeUpdates) { + sendTradeUpdatesListenMessage(); + } + } + + @Override + protected void sendAuthenticationMessage() { + // Ensure that the authorization Future exists + getAuthorizationFuture(); + + final JsonObject authObject = new JsonObject(); + authObject.addProperty("action", "authenticate"); + final JsonObject authData = new JsonObject(); + if (oAuthToken != null) { + authData.addProperty("oauth_token", oAuthToken); + } else { + authData.addProperty("key_id", keyID); + authData.addProperty("secret_key", secretKey); + } + authObject.add("data", authData); + + LOGGER.info("{} websocket sending authentication message...", websocketName); + sendWebsocketMessage(authObject.toString()); + } + + @Override + public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString byteString) { // Binary framing + final String messageString = byteString.utf8(); + LOGGER.trace("Websocket message received: message={}", messageString); + + // Parse JSON string and identify 'messageType' + final JsonObject messageObject = parseString(messageString).getAsJsonObject(); + final UpdatesMessageType messageType = + getGson().fromJson(messageObject.get("stream"), UpdatesMessageType.class); + + // Deserialize message based on 'messageType' and call listener + switch (messageType) { + case AUTHORIZATION: + final AuthorizationMessage authorizationMessage = + getGson().fromJson(messageObject, AuthorizationMessage.class); + authenticated = authorizationMessage.getData().getAction().equalsIgnoreCase("authenticate") && + authorizationMessage.getData().getStatus().equalsIgnoreCase("authorized"); + if (authenticationMessageFuture != null) { + authenticationMessageFuture.complete(authenticated); + } + if (!authenticated) { + throw new RuntimeException(websocketName + " websocket authentication failed!"); + } else { + LOGGER.info("{} websocket authenticated.", websocketName); + } + break; + case LISTENING: + break; + case TRADE_UPDATES: + if (listener != null) { + listener.onTradeUpdate(getGson().fromJson(messageObject, TradeUpdateMessage.class)); + } + break; + default: + throw new UnsupportedOperationException(); + } + } + + @Override + public void setListener(UpdatesListener listener) { + this.listener = listener; + } + + @Override + public void subscribeToTradeUpdates(boolean subscribe) { + listenToTradeUpdates = subscribe; + sendTradeUpdatesListenMessage(); + } + + private void sendTradeUpdatesListenMessage() { + if (!isConnected()) { + throw new IllegalStateException("This websocket must be connected before subscribing to streams!"); + } + final JsonObject requestObject = new JsonObject(); + requestObject.addProperty("action", "listen"); + final JsonArray streamsArray = new JsonArray(); + if (listenToTradeUpdates) { + streamsArray.add(TRADE_UPDATES.toString()); + } + final JsonObject dataObject = new JsonObject(); + dataObject.add("streams", streamsArray); + requestObject.add("data", dataObject); + + sendWebsocketMessage(requestObject.toString()); + LOGGER.info("Requested streams: streams={}.", streamsArray); + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/updates/UpdatesWebsocketInterface.java b/src/main/java/net/jacobpeterson/alpaca/websocket/updates/UpdatesWebsocketInterface.java new file mode 100644 index 00000000..0c86554c --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/updates/UpdatesWebsocketInterface.java @@ -0,0 +1,24 @@ +package net.jacobpeterson.alpaca.websocket.updates; + +import net.jacobpeterson.alpaca.model.websocket.updates.model.UpdatesMessageType; +import net.jacobpeterson.alpaca.websocket.AlpacaWebsocketInterface; + +/** + * {@link UpdatesWebsocketInterface} is an {@link AlpacaWebsocketInterface} for {@link UpdatesWebsocket}. + */ +public interface UpdatesWebsocketInterface extends AlpacaWebsocketInterface { + + /** + * Sets the {@link UpdatesListener}. + * + * @param listener the {@link UpdatesListener} + */ + void setListener(UpdatesListener listener); + + /** + * Subscribes to {@link UpdatesMessageType#TRADE_UPDATES}. + * + * @param subscribe true to subscribe, false otherwise + */ + void subscribeToTradeUpdates(boolean subscribe); +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/authorization/authorization_data.json b/src/main/java/net/jacobpeterson/alpaca/websocket/updates/model/authorization/authorization_data.json similarity index 100% rename from src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/authorization/authorization_data.json rename to src/main/java/net/jacobpeterson/alpaca/websocket/updates/model/authorization/authorization_data.json diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/authorization/authorization_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/updates/model/authorization/authorization_message.json similarity index 66% rename from src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/authorization/authorization_message.json rename to src/main/java/net/jacobpeterson/alpaca/websocket/updates/model/authorization/authorization_message.json index 82edef4d..4c991d10 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/authorization/authorization_message.json +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/updates/model/authorization/authorization_message.json @@ -1,12 +1,12 @@ { "type": "object", "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.trades.model.TradesStreamMessage" + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.updates.model.UpdatesMessage" }, "properties": { "data": { - "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.trades.model.authorization.AuthorizationData", - "title": "The {@link net.jacobpeterson.alpaca.model.websocket.trades.model.authorization.AuthorizationData}." + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.updates.model.authorization.AuthorizationData", + "title": "The {@link net.jacobpeterson.alpaca.model.websocket.updates.model.authorization.AuthorizationData}." } } } diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/updates/model/listening/listening_data.json b/src/main/java/net/jacobpeterson/alpaca/websocket/updates/model/listening/listening_data.json new file mode 100644 index 00000000..b2798264 --- /dev/null +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/updates/model/listening/listening_data.json @@ -0,0 +1,9 @@ +{ + "type": "object", + "properties": { + "streams": { + "existingJavaType": "java.util.Set", + "title": "The {@link java.util.Set} of {@link net.jacobpeterson.alpaca.model.websocket.updates.model.UpdatesMessageType}s." + } + } +} diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/listening/listening_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/updates/model/listening/listening_message.json similarity index 69% rename from src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/listening/listening_message.json rename to src/main/java/net/jacobpeterson/alpaca/websocket/updates/model/listening/listening_message.json index 58b7b54f..0590794f 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/listening/listening_message.json +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/updates/model/listening/listening_message.json @@ -1,12 +1,12 @@ { "type": "object", "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.trades.model.TradesStreamMessage" + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.updates.model.UpdatesMessage" }, "properties": { "data": { - "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.trades.model.listening.ListeningData", - "title": "The {@link net.jacobpeterson.alpaca.model.websocket.trades.model.listening.ListeningData}." + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.updates.model.listening.ListeningData", + "title": "The {@link net.jacobpeterson.alpaca.model.websocket.updates.model.listening.ListeningData}." } } } diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/tradeupdate/trade_update.json b/src/main/java/net/jacobpeterson/alpaca/websocket/updates/model/tradeupdate/trade_update.json similarity index 90% rename from src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/tradeupdate/trade_update.json rename to src/main/java/net/jacobpeterson/alpaca/websocket/updates/model/tradeupdate/trade_update.json index ab5b6875..27f86dc2 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/tradeupdate/trade_update.json +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/updates/model/tradeupdate/trade_update.json @@ -2,8 +2,8 @@ "type": "object", "properties": { "event": { - "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.trades.model.tradeupdate.TradeUpdateEvent", - "title": "The {@link net.jacobpeterson.alpaca.model.websocket.trades.model.tradeupdate.TradeUpdateEvent}." + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.updates.model.tradeupdate.TradeUpdateEvent", + "title": "The {@link net.jacobpeterson.alpaca.model.websocket.updates.model.tradeupdate.TradeUpdateEvent}." }, "execution_id": { "existingJavaType": "java.lang.String", diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/tradeupdate/trade_update_event.json b/src/main/java/net/jacobpeterson/alpaca/websocket/updates/model/tradeupdate/trade_update_event.json similarity index 100% rename from src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/tradeupdate/trade_update_event.json rename to src/main/java/net/jacobpeterson/alpaca/websocket/updates/model/tradeupdate/trade_update_event.json diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/tradeupdate/trade_update_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/updates/model/tradeupdate/trade_update_message.json similarity index 69% rename from src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/tradeupdate/trade_update_message.json rename to src/main/java/net/jacobpeterson/alpaca/websocket/updates/model/tradeupdate/trade_update_message.json index 6876c589..af22fa58 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/tradeupdate/trade_update_message.json +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/updates/model/tradeupdate/trade_update_message.json @@ -1,12 +1,12 @@ { "type": "object", "extends": { - "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.trades.model.TradesStreamMessage" + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.updates.model.UpdatesMessage" }, "properties": { "data": { - "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.trades.model.tradeupdate.TradeUpdate", - "title": "The {@link net.jacobpeterson.alpaca.model.websocket.trades.model.tradeupdate.TradeUpdate}." + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.updates.model.tradeupdate.TradeUpdate", + "title": "The {@link net.jacobpeterson.alpaca.model.websocket.updates.model.tradeupdate.TradeUpdate}." } } } diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/trades_stream_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/updates/model/updates_message.json similarity index 69% rename from src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/trades_stream_message.json rename to src/main/java/net/jacobpeterson/alpaca/websocket/updates/model/updates_message.json index fa337195..ad625676 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/trades_stream_message.json +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/updates/model/updates_message.json @@ -2,8 +2,8 @@ "type": "object", "properties": { "stream": { - "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.trades.model.TradesStreamMessageType", - "title": "The {@link net.jacobpeterson.alpaca.model.websocket.trades.model.TradesStreamMessageType}." + "existingJavaType": "net.jacobpeterson.alpaca.model.websocket.updates.model.UpdatesMessageType", + "title": "The {@link net.jacobpeterson.alpaca.model.websocket.updates.model.UpdatesMessageType}." } } } diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/trades_stream_message_type.json b/src/main/java/net/jacobpeterson/alpaca/websocket/updates/model/updates_message_type.json similarity index 100% rename from src/main/java/net/jacobpeterson/alpaca/websocket/trades/model/trades_stream_message_type.json rename to src/main/java/net/jacobpeterson/alpaca/websocket/updates/model/updates_message_type.json From ee5f1ae3fd8d1e95a8271f1d8a349b2b35ae712d Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Thu, 7 Mar 2024 22:06:54 -0800 Subject: [PATCH 65/84] Refactor REST API wrapper classes packages --- src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java | 3 +++ .../alpaca/{ => rest/broker}/AlpacaBrokerAPI.java | 6 +++--- .../alpaca/rest/broker/{ => events}/EventsApiSSE.java | 2 +- .../alpaca/{ => rest/marketdata}/AlpacaMarketDataAPI.java | 4 ++-- .../alpaca/{ => rest/trader}/AlpacaTraderAPI.java | 4 ++-- 5 files changed, 11 insertions(+), 8 deletions(-) rename src/main/java/net/jacobpeterson/alpaca/{ => rest/broker}/AlpacaBrokerAPI.java (97%) rename src/main/java/net/jacobpeterson/alpaca/rest/broker/{ => events}/EventsApiSSE.java (99%) rename src/main/java/net/jacobpeterson/alpaca/{ => rest/marketdata}/AlpacaMarketDataAPI.java (97%) rename src/main/java/net/jacobpeterson/alpaca/{ => rest/trader}/AlpacaTraderAPI.java (97%) diff --git a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java index 2ae2e22e..c1c73096 100644 --- a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java +++ b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java @@ -3,6 +3,9 @@ import net.jacobpeterson.alpaca.model.util.apitype.BrokerAPIEndpointType; import net.jacobpeterson.alpaca.model.util.apitype.MarketDataWebsocketSourceType; import net.jacobpeterson.alpaca.model.util.apitype.TraderAPIEndpointType; +import net.jacobpeterson.alpaca.rest.broker.AlpacaBrokerAPI; +import net.jacobpeterson.alpaca.rest.marketdata.AlpacaMarketDataAPI; +import net.jacobpeterson.alpaca.rest.trader.AlpacaTraderAPI; import net.jacobpeterson.alpaca.websocket.marketdata.streams.crypto.CryptoMarketDataWebsocket; import net.jacobpeterson.alpaca.websocket.marketdata.streams.crypto.CryptoMarketDataWebsocketInterface; import net.jacobpeterson.alpaca.websocket.marketdata.streams.news.NewsMarketDataWebsocket; diff --git a/src/main/java/net/jacobpeterson/alpaca/AlpacaBrokerAPI.java b/src/main/java/net/jacobpeterson/alpaca/rest/broker/AlpacaBrokerAPI.java similarity index 97% rename from src/main/java/net/jacobpeterson/alpaca/AlpacaBrokerAPI.java rename to src/main/java/net/jacobpeterson/alpaca/rest/broker/AlpacaBrokerAPI.java index 942d71b3..c2d6aa2e 100644 --- a/src/main/java/net/jacobpeterson/alpaca/AlpacaBrokerAPI.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/broker/AlpacaBrokerAPI.java @@ -1,4 +1,4 @@ -package net.jacobpeterson.alpaca; +package net.jacobpeterson.alpaca.rest.broker; import net.jacobpeterson.alpaca.model.util.apitype.BrokerAPIEndpointType; import net.jacobpeterson.alpaca.openapi.broker.ApiClient; @@ -19,7 +19,7 @@ import net.jacobpeterson.alpaca.openapi.broker.api.ReportingApi; import net.jacobpeterson.alpaca.openapi.broker.api.TradingApi; import net.jacobpeterson.alpaca.openapi.broker.api.WatchlistApi; -import net.jacobpeterson.alpaca.rest.broker.EventsApiSSE; +import net.jacobpeterson.alpaca.rest.broker.events.EventsApiSSE; import okhttp3.OkHttpClient; import static com.google.common.base.Preconditions.checkNotNull; @@ -61,7 +61,7 @@ public class AlpacaBrokerAPI { * instance */ @SuppressWarnings("UnnecessaryDefault") - AlpacaBrokerAPI(String brokerAPIKey, String brokerAPISecret, BrokerAPIEndpointType brokerAPIEndpointType, + public AlpacaBrokerAPI(String brokerAPIKey, String brokerAPISecret, BrokerAPIEndpointType brokerAPIEndpointType, OkHttpClient okHttpClient) { checkNotNull(brokerAPIKey); checkNotNull(brokerAPISecret); diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/broker/EventsApiSSE.java b/src/main/java/net/jacobpeterson/alpaca/rest/broker/events/EventsApiSSE.java similarity index 99% rename from src/main/java/net/jacobpeterson/alpaca/rest/broker/EventsApiSSE.java rename to src/main/java/net/jacobpeterson/alpaca/rest/broker/events/EventsApiSSE.java index ea2341e5..5bf918de 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/broker/EventsApiSSE.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/broker/events/EventsApiSSE.java @@ -1,4 +1,4 @@ -package net.jacobpeterson.alpaca.rest.broker; +package net.jacobpeterson.alpaca.rest.broker.events; import com.google.gson.reflect.TypeToken; import net.jacobpeterson.alpaca.openapi.broker.ApiClient; diff --git a/src/main/java/net/jacobpeterson/alpaca/AlpacaMarketDataAPI.java b/src/main/java/net/jacobpeterson/alpaca/rest/marketdata/AlpacaMarketDataAPI.java similarity index 97% rename from src/main/java/net/jacobpeterson/alpaca/AlpacaMarketDataAPI.java rename to src/main/java/net/jacobpeterson/alpaca/rest/marketdata/AlpacaMarketDataAPI.java index 1728c09f..4206b485 100644 --- a/src/main/java/net/jacobpeterson/alpaca/AlpacaMarketDataAPI.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/marketdata/AlpacaMarketDataAPI.java @@ -1,4 +1,4 @@ -package net.jacobpeterson.alpaca; +package net.jacobpeterson.alpaca.rest.marketdata; import net.jacobpeterson.alpaca.openapi.marketdata.ApiClient; import net.jacobpeterson.alpaca.openapi.marketdata.api.CorporateActionsApi; @@ -38,7 +38,7 @@ public class AlpacaMarketDataAPI { * @param brokerAPISecret the Broker API secret * @param okHttpClient an existing {@link OkHttpClient} or null to create a new default instance */ - AlpacaMarketDataAPI(String traderKeyID, String traderSecretKey, + public AlpacaMarketDataAPI(String traderKeyID, String traderSecretKey, String brokerAPIKey, String brokerAPISecret, OkHttpClient okHttpClient) { checkArgument((traderKeyID != null && traderSecretKey != null) ^ (brokerAPIKey != null && brokerAPISecret != null), diff --git a/src/main/java/net/jacobpeterson/alpaca/AlpacaTraderAPI.java b/src/main/java/net/jacobpeterson/alpaca/rest/trader/AlpacaTraderAPI.java similarity index 97% rename from src/main/java/net/jacobpeterson/alpaca/AlpacaTraderAPI.java rename to src/main/java/net/jacobpeterson/alpaca/rest/trader/AlpacaTraderAPI.java index 07485e9c..515ba377 100644 --- a/src/main/java/net/jacobpeterson/alpaca/AlpacaTraderAPI.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/trader/AlpacaTraderAPI.java @@ -1,4 +1,4 @@ -package net.jacobpeterson.alpaca; +package net.jacobpeterson.alpaca.rest.trader; import net.jacobpeterson.alpaca.model.util.apitype.TraderAPIEndpointType; import net.jacobpeterson.alpaca.openapi.trader.ApiClient; @@ -48,7 +48,7 @@ public class AlpacaTraderAPI { * instance */ @SuppressWarnings("UnnecessaryDefault") - AlpacaTraderAPI(String traderKeyID, String traderSecretKey, String traderOAuthToken, + public AlpacaTraderAPI(String traderKeyID, String traderSecretKey, String traderOAuthToken, TraderAPIEndpointType traderAPIEndpointType, OkHttpClient okHttpClient) { checkArgument((traderKeyID != null && traderSecretKey != null) ^ (traderOAuthToken != null), "You must specify a (trader key ID and secret key) or an (OAuth token)!"); From 09d7a15c640c10c2139211c1b6bc3220ebfc7660 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Fri, 8 Mar 2024 10:53:25 -0800 Subject: [PATCH 66/84] AlpacaWebsocket bug fix, change OpenAPI client methods visibility --- build.gradle | 16 ++++++++++++++++ .../alpaca/rest/broker/events/EventsApiSSE.java | 4 ++-- .../alpaca/websocket/AlpacaWebsocket.java | 5 +++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index c6c53daa..2048c1aa 100644 --- a/build.gradle +++ b/build.gradle @@ -229,6 +229,22 @@ final def fixOpenAPIGeneratedClientIssuesTask = tasks.register("fixOpenAPIGenera } } + // Make certain method signatures in the generated client API classes 'protected' instead of 'public' + specIDsOfFileNames.keySet().stream().map { it.replace(".json", "") }.forEach { + for (File apiFile : Paths.get(generatedClientLibrariesPath.getPath(), + it, sourceFilesPath, it.replace("-", ""), "api").toFile().listFiles()) { + // Don't make replacements in the broker 'events' API as that class is wrapped + if (apiFile.name == "EventsApi.java") { + continue + } + // Make replacements + apiFile.text = apiFile.text + .replace("public okhttp3.Call", "protected okhttp3.Call") + .replace("public ApiResponse", "protected ApiResponse") + .replaceAll("\\n.*public ApiClient[\\s\\S]*?customBaseUrl;\\n {4}}", "") + } + } + // Remove the necessary authentication header logic from the generate client's ApiClient specIDsOfFileNames.keySet().stream().map { it.replace(".json", "") }.forEach { final def apiClientFile = Paths.get(generatedClientLibrariesPath.getPath(), it, sourceFilesPath, diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/broker/events/EventsApiSSE.java b/src/main/java/net/jacobpeterson/alpaca/rest/broker/events/EventsApiSSE.java index 5bf918de..65efee8f 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/broker/events/EventsApiSSE.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/broker/events/EventsApiSSE.java @@ -169,12 +169,12 @@ public void onEvent(@NotNull EventSource eventSource, @Nullable String id, @Null @Override public void onFailure(@NotNull EventSource eventSource, @Nullable Throwable throwable, @Nullable Response response) { - LOGGER.error("Event source failure: eventSource={} throwable={}, response={}", - eventSource, throwable, response); if (throwable != null && throwable.getMessage().equals("canceled")) { sseListener.onClose(); return; } + LOGGER.error("Event source failure: eventSource={} throwable={}, response={}", + eventSource, throwable, response); sseListener.onError(throwable, response); } diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java index 518435c2..e4440fbb 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java @@ -147,6 +147,11 @@ public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String rea @Override public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable cause, @Nullable Response response) { + if (intentionalClose) { + onClosed(webSocket, WEBSOCKET_NORMAL_CLOSURE_CODE, WEBSOCKET_NORMAL_CLOSURE_MESSAGE); + return; + } + LOGGER.error("{} websocket failure!", websocketName, cause); // A websocket failure occurs when either there is a connection failure or when the client throws // an exception when receiving a message. In either case, OkHttp will close the websocket connection, From f80a4292f78bb006fc106d0c3c40cc3b6e722013 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Fri, 8 Mar 2024 10:56:21 -0800 Subject: [PATCH 67/84] Update README --- README.md | 209 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 196 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 3aa3cfcc..25c7d6ff 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@

# Overview -This library is a Java client implementation of the Alpaca API. Alpaca lets you trade with algorithms, connect with apps, and build services all with a commission-free trading API for stocks, crypto, and options. This library uses the [Alpaca OpenAPI Specifications](https://docs.alpaca.markets/v1.1/openapi) to generate clients for the REST API with the [OkHttp](https://square.github.io/okhttp/) library, but implements the websocket and SSE streaming interface using a custom implementation with the [OkHttp](https://square.github.io/okhttp/) library. This library is community developed and if you have any questions, please ask them on [Github Discussions](https://github.com/Petersoj/alpaca-java/discussions), the [Alpaca Slack #dev-alpaca-java channel](https://alpaca.markets/slack), or on the [Alpaca Forums](https://forum.alpaca.markets/). +This library is a Java client implementation of the Alpaca API. Alpaca lets you trade with algorithms, connect with apps, and build services all with a commission-free trading API for stocks, crypto, and options. This library uses the [Alpaca OpenAPI Specifications](https://docs.alpaca.markets/v1.1/openapi) to generate clients for the REST API with the [OkHttp](https://square.github.io/okhttp/) library, but implements the websocket and SSE streaming interface using a custom implementation with the [OkHttp](https://square.github.io/okhttp/) library. This library is community developed and if you have any questions, please ask them on [Github Discussions](https://github.com/Petersoj/alpaca-java/discussions), the [Alpaca Slack #dev-alpaca-java channel](https://alpaca.markets/slack), or on the [Alpaca Forums](https://forum.alpaca.markets/). This library strives to provide the complete Alpaca API as a Java client implementation, so open a [new issue](https://github.com/Petersoj/alpaca-java/issues) or [new pull request](https://github.com/Petersoj/alpaca-java/pulls) if you find something missing. Give this repository a star ⭐ if it helped you build a trading algorithm in Java! @@ -33,6 +33,9 @@ Note that you don't have to use the Maven Central artifacts. Instead, you can cl # Logger For logging, this library uses [SLF4j](http://www.slf4j.org/) which serves as an interface for various logging frameworks. This enables you to use whatever logging framework you would like. However, if you do not add a logging framework as a dependency in your project, the console will output a message stating that SLF4j is defaulting to a no-operation (NOP) logger implementation. To enable logging, add a logging framework of your choice as a dependency to your project such as [Logback](http://logback.qos.ch/), [Log4j 2](http://logging.apache.org/log4j/2.x/index.html), [SLF4j-simple](http://www.slf4j.org/manual.html), or [Apache Commons Logging](https://commons.apache.org/proper/commons-logging/). +# `BigDecimal` vs. `Double` +It is generally considered bad practice to represent currency values in floating-point data types such as `float` or `double` because it can lead to [rounding errors](https://ta4j.github.io/ta4j-wiki/Num.html) and [precision loss](https://stackoverflow.com/a/3730040/4352701) in calculations. However, using floating-point data types can have significant performance benefits compared to using arbitrary-precision number data types, especially in a quantitative finance and algorithmic trading environment. Because of this, `alpaca-java` uses the `Double` data type (the `double` boxed type) when using the Market Data APIs and the `BigDecimal` type when using the Trading or Broker APIs. The thinking behind this is that exact decimal quantities are important when placing real trades with real money, but less important when performing calculations of financial indicators. The best solution would be to use the [TA4j](https://github.com/ta4j/ta4j) `Num` interface so that you can decide what data type to use based on your use case, but that is on the [TODO list](#todo) for now. + # Examples Note that the examples below are not exhaustive. Refer to the [Javadoc](https://javadoc.io/doc/net.jacobpeterson.alpaca/alpaca-java) for all classes and method signatures. @@ -42,25 +45,204 @@ Note that the examples below are not exhaustive. Refer to the [Javadoc](https:// The Alpaca API documentation is located [here](https://docs.alpaca.markets/) and the [`AlpacaAPI`](src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java) Javadoc is located [here](https://javadoc.io/doc/net.jacobpeterson.alpaca/alpaca-java/latest/net/jacobpeterson/alpaca/AlpacaAPI.html). Example usage: + +Use this code if you are using the Trading or Market Data APIs for a single Alpaca account: ```java -// TODO +final String keyID = ""; +final String secretKey = ""; +final TraderAPIEndpointType endpointType = TraderAPIEndpointType.PAPER; // or 'LIVE' +final MarketDataWebsocketSourceType sourceType = MarketDataWebsocketSourceType.IEX; // or 'SIP' +final AlpacaAPI alpacaAPI = new AlpacaAPI(keyID, secretKey, endpointType, sourceType); ``` -Note that this library uses [OkHttp](https://square.github.io/okhttp/) as its HTTP client library which creates background threads to service requests via a connection pool. These threads persist even if the main thread exits, so if you want to destroy these threads when you're done using [`AlpacaAPI`](src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java), call `alpacaAPI.closeOkHttpClient();`. +Use this code if you are using the Trading API with OAuth to act on behalf of an Alpaca account: +```java +final String oAuthToken = ""; +final TraderAPIEndpointType endpointType = TraderAPIEndpointType.PAPER; // or 'LIVE' +final AlpacaAPI alpacaAPI = new AlpacaAPI(oAuthToken, endpointType); +``` -## Trading API +Use this code if you are using the Broker API: ```java -// TODO +final String brokerAPIKey = ""; +final String brokerAPISecret = ""; +final BrokerAPIEndpointType endpointType = BrokerAPIEndpointType.SANDBOX; // or 'PRODUCTION' +final AlpacaAPI alpacaAPI = new AlpacaAPI(brokerAPIKey, brokerAPISecret, endpointType); ``` -## Market Data API +Note that this library uses [OkHttp](https://square.github.io/okhttp/) as its HTTP client library which creates background threads to service requests via a connection pool. These threads persist even if the main thread exits, so if you want to destroy these threads when you're done using [`AlpacaAPI`](src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java), use `alpacaAPI.closeOkHttpClient();`. + +## [`Trader API`](src/main/java/net/jacobpeterson/alpaca/rest/trader/AlpacaTraderAPI.java) +The [`Trader API`](src/main/java/net/jacobpeterson/alpaca/rest/trader/AlpacaTraderAPI.java) is used for placing trades, updating account details, getting open positions, and more. Refer to the [Javadoc](https://javadoc.io/doc/net.jacobpeterson.alpaca/alpaca-java/latest/net/jacobpeterson/alpaca/rest/trader/AlpacaTraderAPI.html) for a list of all available method signatures. + +Example usage: ```java -// TODO +// Place a market order to buy one share of Apple +final Order openingOrder = alpacaAPI.trader().orders() + .postOrder(new PostOrderRequest() + .symbol("AAPL") + .qty("1") + .side(OrderSide.BUY) + .type(OrderType.MARKET) + .timeInForce(TimeInForce.GTC)); +System.out.println("Opening Apple order: " + openingOrder); + +// Wait for massive gains +Thread.sleep(10_000); + +// Close the Apple position +Order closingOrder = alpacaAPI.trader().positions() + .deleteOpenPosition("AAPL", null, new BigDecimal("100")); +System.out.println("Closing Apple order: " + openingOrder); + +// Wait for closing trade to fill +Thread.sleep(10_000); + +// Print out PnL +final String openFillPrice = alpacaAPI.trader().orders() + .getOrderByOrderID(UUID.fromString(openingOrder.getId()), false) + .getFilledAvgPrice(); +final String closeFillPrice = alpacaAPI.trader().orders() + .getOrderByOrderID(UUID.fromString(closingOrder.getId()), false) + .getFilledAvgPrice(); +System.out.println("PnL from Apple trade: " + + new BigDecimal(closeFillPrice).subtract(new BigDecimal(openFillPrice))); ``` -## Broker API +## [`Market Data API`](src/main/java/net/jacobpeterson/alpaca/rest/marketdata/AlpacaMarketDataAPI.java) +The [`Market Data API`](src/main/java/net/jacobpeterson/alpaca/rest/marketdata/AlpacaMarketDataAPI.java) is used getting market data for stocks, cryptocurrencies, options, and more. Refer to the [Javadoc](https://javadoc.io/doc/net.jacobpeterson.alpaca/alpaca-java/latest/net/jacobpeterson/alpaca/rest/marketdata/AlpacaMarketDataAPI.html) for a list of all available method signatures. + +Example usage: ```java -// TODO +// Print out the latest Tesla trade +final StockTrade latestTSLATrade = alpacaAPI.marketData().stock() + .stockLatestTradeSingle("TSLA", StockFeed.IEX, null).getTrade(); +System.out.printf("Latest TSLA trade: price=%s, size=%s\n", + latestTSLATrade.getP(), latestTSLATrade.getS()); + +// Print out the highest Bitcoin ask price on the order book +final CryptoOrderbook latestBitcoinOrderBooks = alpacaAPI.marketData().crypto() + .cryptoLatestOrderbooks(CryptoLoc.US, "BTC/USD").getOrderbooks().get("BTC/USD"); +final Double highestBitcoinAskPrice = latestBitcoinOrderBooks.getA().stream() + .map(CryptoOrderbookEntry::getP) + .max(Double::compare) + .orElse(null); +System.out.println("Bitcoin highest ask price: " + highestBitcoinAskPrice); + +// Print out the latest Microsoft option trade +final String latestMSFTOptionTrade = alpacaAPI.marketData().option() + .optionChain("MSFT").getSnapshots().entrySet().stream() + .filter(entry -> entry.getValue().getLatestTrade() != null) + .map(entry -> Map.entry(entry.getKey(), entry.getValue().getLatestTrade())) + .max(Comparator.comparing(entry -> entry.getValue().getT())) + .map(entry -> String.format("ticker=%s, time=%s, price=%s", + entry.getKey(), entry.getValue().getT(), entry.getValue().getP())) + .orElse(null); +System.out.println("Latest Microsoft option trade: " + latestMSFTOptionTrade); +``` + +## [`Broker API`](src/main/java/net/jacobpeterson/alpaca/rest/broker/AlpacaBrokerAPI.java) +The [`Broker API`](src/main/java/net/jacobpeterson/alpaca/rest/broker/AlpacaBrokerAPI.java) is used for creating new Alpaca accounts for your end users, funding their accounts, placing trades, and more. Refer to the [Javadoc](https://javadoc.io/doc/net.jacobpeterson.alpaca/alpaca-java/latest/net/jacobpeterson/alpaca/rest/broker/AlpacaBrokerAPI.html) for a list of all available method signatures. + +Example usage: +```java +// Listen to the trade events (SSE) and print them out +final SSERequest sseRequest = alpacaAPI.broker().events() + .subscribeToTradeV2(null, null, null, null, new SSEListenerAdapter<>()); + +// Wait for SSE channel to be ready +Thread.sleep(2000); + +// Buy one share of GME for an account +alpacaAPI.broker().trading() + .createOrderForAccount(UUID.fromString(""), + new CreateOrderRequest() + .symbol("GME") + .qty(BigDecimal.ONE) + .side(OrderSide.SELL) + .timeInForce(TimeInForce.GTC) + .type(OrderType.MARKET)); +// Wait to be filled +Thread.sleep(2000); + +// Close the SSE stream and the OkHttpClient to exit cleanly +sseRequest.close(); +alpacaAPI.closeOkHttpClient(); +``` + +## [`Updates Stream`](src/main/java/net/jacobpeterson/alpaca/websocket/updates/UpdatesWebsocket.java) +The [`Updates Stream`](src/main/java/net/jacobpeterson/alpaca/websocket/updates/UpdatesWebsocket.java) is used for listening to trade updates in realtime. Refer to the [Javadoc](https://javadoc.io/doc/net.jacobpeterson.alpaca/alpaca-java/latest/net/jacobpeterson/alpaca/websocket/updates/UpdatesWebsocketInterface.html) for a list of all available method signatures. + +Example usage: +```java +// Connect to the 'updates' stream and wait until it's authorized +alpacaAPI.updatesStream().connect(); +if (!alpacaAPI.updatesStream().waitForAuthorization(5, TimeUnit.SECONDS)) { + throw new RuntimeException(); +} + +// Print out trade updates +alpacaAPI.updatesStream().setListener(System.out::println); +alpacaAPI.updatesStream().subscribeToTradeUpdates(true); + +// Place a trade +alpacaAPI.trader().orders().postOrder(new PostOrderRequest() + .symbol("AAPL") + .qty("1") + .side(OrderSide.BUY) + .type(OrderType.MARKET) + .timeInForce(TimeInForce.GTC)); + +// Wait a few seconds +Thread.sleep(5000); + +// Close the trade +alpacaAPI.trader().positions().deleteAllOpenPositions(true); + +// Wait a few seconds +Thread.sleep(5000); + +// Disconnect the 'updates' stream and exit cleanly +alpacaAPI.updatesStream().disconnect(); +alpacaAPI.closeOkHttpClient(); +``` + +## [`Market Data Stream`](src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java) +The [`Market Data Stream`](src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java) is used for listening to stock market data, crypto market data, and news data in realtime. Refer to the Javadocs ([stock](https://javadoc.io/doc/net.jacobpeterson.alpaca/alpaca-java/latest/net/jacobpeterson/alpaca/websocket/marketdata/streams/stock/StockMarketDataWebsocketInterface.html), [crypto](https://javadoc.io/doc/net.jacobpeterson.alpaca/alpaca-java/latest/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/CryptoMarketDataWebsocketInterface.html), [news](https://javadoc.io/doc/net.jacobpeterson.alpaca/alpaca-java/latest/net/jacobpeterson/alpaca/websocket/marketdata/streams/news/NewsMarketDataWebsocketInterface.html)) for a list of all available method signatures. + +Example usage: +```java +// Connect to the 'stock market data' stream and wait until it's authorized +alpacaAPI.stockMarketDataStream().connect(); +if (!alpacaAPI.stockMarketDataStream().waitForAuthorization(5, TimeUnit.SECONDS)) { + throw new RuntimeException(); +} + +// Print out trade messages +alpacaAPI.stockMarketDataStream().setListener(new StockMarketDataListenerAdapter() { + @Override + public void onTrade(StockTradeMessage trade) { + System.out.println("Received trade: " + trade); + } +}); + +// Subscribe to AAPL trades +System.out.println("Subscribed to Apple trades."); +alpacaAPI.stockMarketDataStream().setTradeSubscriptions(Set.of("AAPL")); + +// Wait a few seconds +Thread.sleep(5000); + +// Unsubscribe from AAPL and subscribe to TSLA and MSFT +System.out.println("Subscribed to Tesla and Microsoft trades."); +alpacaAPI.stockMarketDataStream().setTradeSubscriptions(Set.of("TSLA", "MSFT")); + +// Wait a few seconds +Thread.sleep(5000); + +// Disconnect the 'stock market data' stream and exit cleanly +alpacaAPI.stockMarketDataStream().disconnect(); +alpacaAPI.closeOkHttpClient(); ``` # Building @@ -75,12 +257,13 @@ To install the built artifacts to your local Maven repository on your machine (t ``` # TODO -- Implement better reconnect logic for Websockets and SSE streaming -- Implement Unit Testing for REST API and Websocket streaming (both live and mocked) +- Implement better reconnect logic for Websockets and SSE streaming. +- Implement Unit Testing for REST API and Websocket streaming (both live and mocked). +- Use [TA4j](https://github.com/ta4j/ta4j) `Num` interface instead of `Double` or `BigDecimal` for number data types so that users can use either `Double` or `BigDecimal` for performance or precision in price data. # Contributing Contributions are welcome! -Do the following before starting your Pull Request: +Do the following before starting your pull request: 1. Create a new branch in your forked repository for your feature or bug fix instead of committing directly to the `master` branch in your fork. -2. Use the `dev` branch as the base branch in your Pull Request. +2. Use the `dev` branch as the base branch in your pull request. From a20ba8f8f526e5de954416054907bfa1802f817f Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Fri, 8 Mar 2024 11:05:03 -0800 Subject: [PATCH 68/84] Consolidate throwing exception for disconnected websockets --- README.md | 1 + .../net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java | 3 +++ .../alpaca/websocket/updates/UpdatesWebsocket.java | 3 --- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 25c7d6ff..d3b6dace 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,7 @@ alpacaAPI.broker().trading() .side(OrderSide.SELL) .timeInForce(TimeInForce.GTC) .type(OrderType.MARKET)); + // Wait to be filled Thread.sleep(2000); diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java index e4440fbb..d6f920c2 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/AlpacaWebsocket.java @@ -209,6 +209,9 @@ protected void cleanupState() { * @param message the message */ protected void sendWebsocketMessage(String message) { + if (!isConnected()) { + throw new IllegalStateException("This websocket must be connected before send a message!"); + } LOGGER.trace("Websocket message sent: {}", message); websocket.send(message); } diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/updates/UpdatesWebsocket.java b/src/main/java/net/jacobpeterson/alpaca/websocket/updates/UpdatesWebsocket.java index 99e1db42..ae31a623 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/updates/UpdatesWebsocket.java +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/updates/UpdatesWebsocket.java @@ -153,9 +153,6 @@ public void subscribeToTradeUpdates(boolean subscribe) { } private void sendTradeUpdatesListenMessage() { - if (!isConnected()) { - throw new IllegalStateException("This websocket must be connected before subscribing to streams!"); - } final JsonObject requestObject = new JsonObject(); requestObject.addProperty("action", "listen"); final JsonArray streamsArray = new JsonArray(); From b34f8dd0e2449eec65401792fb988d79a4ab42b3 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Fri, 8 Mar 2024 11:07:52 -0800 Subject: [PATCH 69/84] Undo premature version number change --- README.md | 4 ++-- build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d3b6dace..dade8336 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Give this repository a star ⭐ if it helped you build a trading algorithm in Ja If you are using Gradle as your build tool, add the following dependency to your `build.gradle` file: ``` -implementation group: "net.jacobpeterson.alpaca", name: "alpaca-java", version: "10.0.0" +implementation group: "net.jacobpeterson.alpaca", name: "alpaca-java", version: "9.2.0" ``` If you are using Maven as your build tool, add the following dependency to your `pom.xml` file: @@ -24,7 +24,7 @@ If you are using Maven as your build tool, add the following dependency to your net.jacobpeterson.alpaca alpaca-java - 10.0.0 + 9.2.0 ``` diff --git a/build.gradle b/build.gradle index 2048c1aa..08d8e81d 100644 --- a/build.gradle +++ b/build.gradle @@ -22,7 +22,7 @@ plugins { final def projectGroup = "net.jacobpeterson.alpaca" final def projectArtifactID = "alpaca-java" -final def projectVersion = "10.0.0-SNAPSHOT" +final def projectVersion = "9.2.0-SNAPSHOT" group = projectGroup version = projectVersion From 32495432b5ee3e49c48fdf61ee2c7a1ae59ee9a3 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Fri, 8 Mar 2024 11:08:33 -0800 Subject: [PATCH 70/84] Update to 10.0.0 --- README.md | 4 ++-- build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index dade8336..d3b6dace 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Give this repository a star ⭐ if it helped you build a trading algorithm in Ja If you are using Gradle as your build tool, add the following dependency to your `build.gradle` file: ``` -implementation group: "net.jacobpeterson.alpaca", name: "alpaca-java", version: "9.2.0" +implementation group: "net.jacobpeterson.alpaca", name: "alpaca-java", version: "10.0.0" ``` If you are using Maven as your build tool, add the following dependency to your `pom.xml` file: @@ -24,7 +24,7 @@ If you are using Maven as your build tool, add the following dependency to your net.jacobpeterson.alpaca alpaca-java - 9.2.0 + 10.0.0 ``` diff --git a/build.gradle b/build.gradle index 08d8e81d..2048c1aa 100644 --- a/build.gradle +++ b/build.gradle @@ -22,7 +22,7 @@ plugins { final def projectGroup = "net.jacobpeterson.alpaca" final def projectArtifactID = "alpaca-java" -final def projectVersion = "9.2.0-SNAPSHOT" +final def projectVersion = "10.0.0-SNAPSHOT" group = projectGroup version = projectVersion From 5b9f9e2b20a931d9c108a68d0d999abe9b95c2fb Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Fri, 8 Mar 2024 12:27:15 -0800 Subject: [PATCH 71/84] Minor update to README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d3b6dace..0ad65e93 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Note that you don't have to use the Maven Central artifacts. Instead, you can cl For logging, this library uses [SLF4j](http://www.slf4j.org/) which serves as an interface for various logging frameworks. This enables you to use whatever logging framework you would like. However, if you do not add a logging framework as a dependency in your project, the console will output a message stating that SLF4j is defaulting to a no-operation (NOP) logger implementation. To enable logging, add a logging framework of your choice as a dependency to your project such as [Logback](http://logback.qos.ch/), [Log4j 2](http://logging.apache.org/log4j/2.x/index.html), [SLF4j-simple](http://www.slf4j.org/manual.html), or [Apache Commons Logging](https://commons.apache.org/proper/commons-logging/). # `BigDecimal` vs. `Double` -It is generally considered bad practice to represent currency values in floating-point data types such as `float` or `double` because it can lead to [rounding errors](https://ta4j.github.io/ta4j-wiki/Num.html) and [precision loss](https://stackoverflow.com/a/3730040/4352701) in calculations. However, using floating-point data types can have significant performance benefits compared to using arbitrary-precision number data types, especially in a quantitative finance and algorithmic trading environment. Because of this, `alpaca-java` uses the `Double` data type (the `double` boxed type) when using the Market Data APIs and the `BigDecimal` type when using the Trading or Broker APIs. The thinking behind this is that exact decimal quantities are important when placing real trades with real money, but less important when performing calculations of financial indicators. The best solution would be to use the [TA4j](https://github.com/ta4j/ta4j) `Num` interface so that you can decide what data type to use based on your use case, but that is on the [TODO list](#todo) for now. +It is generally considered bad practice to represent currency values in floating-point data types such as `float` or `double` because it can lead to [rounding errors](https://ta4j.github.io/ta4j-wiki/Num.html) and [precision loss](https://stackoverflow.com/a/3730040/4352701) in calculations. However, using floating-point data types can have significant performance benefits compared to using arbitrary-precision number data types, especially in a quantitative finance and algorithmic trading environment. Because of this, `alpaca-java` uses the `Double` data type (the `double` boxed type) when using the Market Data APIs and the `BigDecimal` data type when using the Trading or Broker APIs. The thinking behind this is that exact decimal quantities are important when placing real trades with real money, but less important when performing calculations of financial indicators. The best solution would be to use the [TA4j](https://github.com/ta4j/ta4j) `Num` interface so that you can decide what data type to use based on your use case, but that is on the [TODO list](#todo) for now. By the way, using `BigDecimal` wouldn't matter if Alpaca used floating-point data types in their internal systems or in their REST APIs, but the fact that they use `String` data types in some of the REST API JSON responses and their Trading and Broker OpenAPI specifications don't use the `double` data type, this leads me to believe that using `BigDecimal` does actually matter. # Examples Note that the examples below are not exhaustive. Refer to the [Javadoc](https://javadoc.io/doc/net.jacobpeterson.alpaca/alpaca-java) for all classes and method signatures. @@ -228,15 +228,15 @@ alpacaAPI.stockMarketDataStream().setListener(new StockMarketDataListenerAdapter }); // Subscribe to AAPL trades -System.out.println("Subscribed to Apple trades."); alpacaAPI.stockMarketDataStream().setTradeSubscriptions(Set.of("AAPL")); +System.out.println("Subscribed to Apple trades."); // Wait a few seconds Thread.sleep(5000); // Unsubscribe from AAPL and subscribe to TSLA and MSFT -System.out.println("Subscribed to Tesla and Microsoft trades."); alpacaAPI.stockMarketDataStream().setTradeSubscriptions(Set.of("TSLA", "MSFT")); +System.out.println("Subscribed to Tesla and Microsoft trades."); // Wait a few seconds Thread.sleep(5000); From 933c28f521d33c3d1681d738840693501a6bac44 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Sat, 9 Mar 2024 08:38:58 -0800 Subject: [PATCH 72/84] Fix OpenAPI generated client 'anyOf' and 'oneOf' issues --- build.gradle | 20 ++++++++--- .../alpaca/rest/broker/AlpacaBrokerAPI.java | 33 +++++++++++++++++++ .../alpaca/rest/trader/AlpacaTraderAPI.java | 9 +++++ 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 2048c1aa..7eadeb3c 100644 --- a/build.gradle +++ b/build.gradle @@ -220,10 +220,15 @@ final def fixOpenAPIGeneratedClientIssuesTask = tasks.register("fixOpenAPIGenera modelFile.text = modelFile.text .replaceAll("/\\*\\*\\n {2}\\* Validates the JSON Element and throws an exception if issues " + "found[\\s\\S]*?\\*/", "") + .replace("import java.io.IOException;", + "import java.io.IOException;\nimport java.util.function.Predicate;") .replace("public static void validateJsonElement(JsonElement jsonElement) throws IOException {", - "public static boolean validate = false;\n public static void " + - "validateJsonElement(JsonElement jsonElement) throws IOException {\n" + - " if (!validate) return;") + "public static boolean validate = false;\n" + + " public static Predicate isValid = jsonElement -> true;\n" + + " public static void validateJsonElement(JsonElement jsonElement) " + + "throws IOException {\n" + + " if (!isValid.test(jsonElement)) throw new RuntimeException();\n" + + " if (!validate) return;\n") .replace("throw new IllegalArgumentException(\"Unexpected value '\" + value + \"'\");", "return null;") } @@ -255,12 +260,19 @@ final def fixOpenAPIGeneratedClientIssuesTask = tasks.register("fixOpenAPIGenera .replaceAll("for \\(String authName : authNames\\)[\\s\\S]*?uri\\);\\n. {7}}", "") } - // Make the 'JSON' classes of the generate clients have a static Gson initializer + // Make the 'JSON' classes of the generate clients have a static Gson initializer. Also fix the OffsetDateTime + // Gson type adapter to accommodate ISO local dates. specIDsOfFileNames.keySet().stream().map { it.replace(".json", "") }.forEach { final def jsonClientFile = Paths.get(generatedClientLibrariesPath.getPath(), it, sourceFilesPath, it.replace("-", ""), "JSON.java").toFile() jsonClientFile.text = jsonClientFile.text .replaceAll("\\{\\n {8}GsonBuilder", "static {\n GsonBuilder") + .replaceAll("}[\\s\\S].*return OffsetDateTime\\.parse\\(date, formatter\\);", + "} else if (!date.contains(\"T\")) {\n" + + "return LocalDate.parse(date, DateTimeFormatter.ISO_LOCAL_DATE)" + + ".atTime(java.time.LocalTime.MIN.atOffset(java.time.ZoneOffset.UTC));\n" + + "}\n" + + "return OffsetDateTime.parse(date, formatter);") } } } diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/broker/AlpacaBrokerAPI.java b/src/main/java/net/jacobpeterson/alpaca/rest/broker/AlpacaBrokerAPI.java index c2d6aa2e..cfbd3e75 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/broker/AlpacaBrokerAPI.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/broker/AlpacaBrokerAPI.java @@ -1,5 +1,6 @@ package net.jacobpeterson.alpaca.rest.broker; +import com.google.gson.JsonElement; import net.jacobpeterson.alpaca.model.util.apitype.BrokerAPIEndpointType; import net.jacobpeterson.alpaca.openapi.broker.ApiClient; import net.jacobpeterson.alpaca.openapi.broker.api.AccountsApi; @@ -19,9 +20,18 @@ import net.jacobpeterson.alpaca.openapi.broker.api.ReportingApi; import net.jacobpeterson.alpaca.openapi.broker.api.TradingApi; import net.jacobpeterson.alpaca.openapi.broker.api.WatchlistApi; +import net.jacobpeterson.alpaca.openapi.broker.model.AdminActionLegacyNote; +import net.jacobpeterson.alpaca.openapi.broker.model.AdminActionLiquidation; +import net.jacobpeterson.alpaca.openapi.broker.model.AdminActionTransactionCancel; +import net.jacobpeterson.alpaca.openapi.broker.model.JNLC; +import net.jacobpeterson.alpaca.openapi.broker.model.JNLS; +import net.jacobpeterson.alpaca.openapi.broker.model.NonTradeActivity; +import net.jacobpeterson.alpaca.openapi.broker.model.TradeActivity; import net.jacobpeterson.alpaca.rest.broker.events.EventsApiSSE; import okhttp3.OkHttpClient; +import java.util.function.BiFunction; + import static com.google.common.base.Preconditions.checkNotNull; import static net.jacobpeterson.alpaca.util.apikey.APIKeyUtil.createBrokerAPIAuthKey; @@ -31,6 +41,29 @@ */ public class AlpacaBrokerAPI { + // Set validation predicates for 'anyOf' and 'oneOf' models + static { + // 'AccountsApi' models + TradeActivity.isValid = jsonElement -> jsonElement.getAsJsonObject().has("type"); + NonTradeActivity.isValid = jsonElement -> !jsonElement.getAsJsonObject().has("type"); + + // 'JournalsApi' models + final BiFunction transferPredicate = (jnlc, jsonElement) -> { + final String entryType = jsonElement.getAsJsonObject().get("entry_type").getAsString(); + return jnlc ? entryType.equals("JNLC") : entryType.equals("JNLS"); + }; + JNLS.isValid = jsonElement -> transferPredicate.apply(true, jsonElement); + JNLC.isValid = jsonElement -> transferPredicate.apply(false, jsonElement); + + // 'EventsApi' models + AdminActionLegacyNote.isValid = jsonElement -> jsonElement.getAsJsonObject() + .get("type").getAsString().contains("note_admin_event"); + AdminActionLiquidation.isValid = jsonElement -> jsonElement.getAsJsonObject() + .get("type").getAsString().equals("liquidation_admin_event"); + AdminActionTransactionCancel.isValid = jsonElement -> jsonElement.getAsJsonObject() + .get("type").getAsString().equals("transaction_cancel_admin_event"); + } + private final ApiClient apiClient; private AccountsApi accounts; private AssetsApi assets; diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/trader/AlpacaTraderAPI.java b/src/main/java/net/jacobpeterson/alpaca/rest/trader/AlpacaTraderAPI.java index 515ba377..7b560a26 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/trader/AlpacaTraderAPI.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/trader/AlpacaTraderAPI.java @@ -13,6 +13,8 @@ import net.jacobpeterson.alpaca.openapi.trader.api.PortfolioHistoryApi; import net.jacobpeterson.alpaca.openapi.trader.api.PositionsApi; import net.jacobpeterson.alpaca.openapi.trader.api.WatchlistsApi; +import net.jacobpeterson.alpaca.openapi.trader.model.NonTradeActivities; +import net.jacobpeterson.alpaca.openapi.trader.model.TradingActivities; import okhttp3.OkHttpClient; import static com.google.common.base.Preconditions.checkArgument; @@ -24,6 +26,13 @@ */ public class AlpacaTraderAPI { + // Set validation predicates for 'anyOf' and 'oneOf' models + static { + // 'AccountConfigurationsApi' models + TradingActivities.isValid = jsonElement -> jsonElement.getAsJsonObject().has("type"); + NonTradeActivities.isValid = jsonElement -> !jsonElement.getAsJsonObject().has("type"); + } + private final ApiClient apiClient; private AccountActivitiesApi accountActivities; private AccountConfigurationsApi accountConfigurations; From 5d205a7cfad21a11d7b14e28b47b4b70fd5a3364 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Sun, 10 Mar 2024 20:10:44 -0700 Subject: [PATCH 73/84] Minor update to README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0ad65e93..c7f1cef2 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ System.out.println("Opening Apple order: " + openingOrder); Thread.sleep(10_000); // Close the Apple position -Order closingOrder = alpacaAPI.trader().positions() +final Order closingOrder = alpacaAPI.trader().positions() .deleteOpenPosition("AAPL", null, new BigDecimal("100")); System.out.println("Closing Apple order: " + openingOrder); From ab2225f88f224cad8a95e39326b97e7f246bddd5 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Tue, 12 Mar 2024 12:10:38 -0700 Subject: [PATCH 74/84] Update websocket `CryptoOrderBookMessage` to add `r` boolean --- README.md | 2 +- .../crypto/model/orderbook/crypto_order_book_message.json | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c7f1cef2..6f855358 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ System.out.println("PnL from Apple trade: " + ``` ## [`Market Data API`](src/main/java/net/jacobpeterson/alpaca/rest/marketdata/AlpacaMarketDataAPI.java) -The [`Market Data API`](src/main/java/net/jacobpeterson/alpaca/rest/marketdata/AlpacaMarketDataAPI.java) is used getting market data for stocks, cryptocurrencies, options, and more. Refer to the [Javadoc](https://javadoc.io/doc/net.jacobpeterson.alpaca/alpaca-java/latest/net/jacobpeterson/alpaca/rest/marketdata/AlpacaMarketDataAPI.html) for a list of all available method signatures. +The [`Market Data API`](src/main/java/net/jacobpeterson/alpaca/rest/marketdata/AlpacaMarketDataAPI.java) is used for getting market data for stocks, cryptocurrencies, options, and more. Refer to the [Javadoc](https://javadoc.io/doc/net.jacobpeterson.alpaca/alpaca-java/latest/net/jacobpeterson/alpaca/rest/marketdata/AlpacaMarketDataAPI.html) for a list of all available method signatures. Example usage: ```java diff --git a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/orderbook/crypto_order_book_message.json b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/orderbook/crypto_order_book_message.json index 76c632fa..79814a8a 100644 --- a/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/orderbook/crypto_order_book_message.json +++ b/src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/streams/crypto/model/orderbook/crypto_order_book_message.json @@ -23,6 +23,11 @@ "existingJavaType": "java.time.OffsetDateTime", "javaName": "timestamp", "title": "The timestamp with nanosecond precision." + }, + "r": { + "existingJavaType": "java.lang.Boolean", + "javaName": "reset", + "title": "Whether this particular {@link net.jacobpeterson.alpaca.model.websocket.marketdata.streams.crypto.model.orderbook.CryptoOrderBookMessage} contains the whole order book and not just an update. Typically true for the first message and false for subsequent messages." } } } From c8d41b72161c8827dc331fe246c8d6de198b7b3c Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Sat, 23 Mar 2024 18:57:10 -0700 Subject: [PATCH 75/84] Minor fixes/changes --- README.md | 2 +- build.gradle | 4 ++-- .../java/net/jacobpeterson/alpaca/util/apikey/APIKeyUtil.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6f855358..e91ae098 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@

# Overview -This library is a Java client implementation of the Alpaca API. Alpaca lets you trade with algorithms, connect with apps, and build services all with a commission-free trading API for stocks, crypto, and options. This library uses the [Alpaca OpenAPI Specifications](https://docs.alpaca.markets/v1.1/openapi) to generate clients for the REST API with the [OkHttp](https://square.github.io/okhttp/) library, but implements the websocket and SSE streaming interface using a custom implementation with the [OkHttp](https://square.github.io/okhttp/) library. This library is community developed and if you have any questions, please ask them on [Github Discussions](https://github.com/Petersoj/alpaca-java/discussions), the [Alpaca Slack #dev-alpaca-java channel](https://alpaca.markets/slack), or on the [Alpaca Forums](https://forum.alpaca.markets/). This library strives to provide the complete Alpaca API as a Java client implementation, so open a [new issue](https://github.com/Petersoj/alpaca-java/issues) or [new pull request](https://github.com/Petersoj/alpaca-java/pulls) if you find something missing. +This library is a Java client implementation of the [Alpaca](https://alpaca.markets) API. Alpaca lets you trade with algorithms, connect with apps, and build services all with a commission-free trading API for stocks, crypto, and options. This library uses the [Alpaca OpenAPI Specifications](https://docs.alpaca.markets/openapi) to generate clients for the REST API with the [OkHttp](https://square.github.io/okhttp/) library, but implements the websocket and SSE streaming interface using a custom implementation with the [OkHttp](https://square.github.io/okhttp/) library. This library is community developed and if you have any questions, please ask them on [Github Discussions](https://github.com/Petersoj/alpaca-java/discussions), the [Alpaca Slack #dev-alpaca-java channel](https://alpaca.markets/slack), or on the [Alpaca Forums](https://forum.alpaca.markets/). This library strives to provide the complete Alpaca API as a Java client implementation, so open a [new issue](https://github.com/Petersoj/alpaca-java/issues) or [new pull request](https://github.com/Petersoj/alpaca-java/pulls) if you find something missing. Give this repository a star ⭐ if it helped you build a trading algorithm in Java! diff --git a/build.gradle b/build.gradle index 7eadeb3c..6009b4a1 100644 --- a/build.gradle +++ b/build.gradle @@ -87,7 +87,7 @@ javadoc { // BEGIN Alpaca OpenAPI Specification (OAS) client generation // -// These IDs come from the URLs on this page: https://docs.alpaca.markets/v1.1/openapi +// These IDs come from the URLs on this page: https://docs.alpaca.markets/openapi final def specIDsOfFileNames = Map.of( "trader.json", "657760422129f9005cf4bd58", "broker.json", "657771fc2471d20070183049", @@ -107,7 +107,7 @@ def downloadOpenAPISpecFilesTask = tasks.register("downloadOpenAPISpecFiles") { // Download spec files specIDsOfFileNames.forEach { fileName, alpacaSpecID -> final def outputFile = new File(specDownloadPath, fileName) - final def specURL = "https://docs.alpaca.markets/v1.1/openapi/${alpacaSpecID}" + final def specURL = "https://docs.alpaca.markets/openapi/${alpacaSpecID}" outputFile.getParentFile().mkdirs() try { logger.info("Downloading OpenAPI spec file from: {}", specURL) diff --git a/src/main/java/net/jacobpeterson/alpaca/util/apikey/APIKeyUtil.java b/src/main/java/net/jacobpeterson/alpaca/util/apikey/APIKeyUtil.java index 29de58ec..c9d0ce07 100644 --- a/src/main/java/net/jacobpeterson/alpaca/util/apikey/APIKeyUtil.java +++ b/src/main/java/net/jacobpeterson/alpaca/util/apikey/APIKeyUtil.java @@ -15,7 +15,7 @@ public class APIKeyUtil { * * @return the key {@link String} * - * @see Alpaca Docs + * @see Alpaca Docs */ public static String createBrokerAPIAuthKey(String brokerAPIKey, String brokerAPISecret) { return Base64.getEncoder().encodeToString((brokerAPIKey + ":" + brokerAPISecret).getBytes()); From ea620b890c7083da0db4c5a40b3abf1ed1ba294e Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Sat, 23 Mar 2024 18:57:55 -0700 Subject: [PATCH 76/84] Update to 10.0.1 --- README.md | 4 ++-- build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e91ae098..fdb5fc2d 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Give this repository a star ⭐ if it helped you build a trading algorithm in Ja If you are using Gradle as your build tool, add the following dependency to your `build.gradle` file: ``` -implementation group: "net.jacobpeterson.alpaca", name: "alpaca-java", version: "10.0.0" +implementation group: "net.jacobpeterson.alpaca", name: "alpaca-java", version: "10.0.1" ``` If you are using Maven as your build tool, add the following dependency to your `pom.xml` file: @@ -24,7 +24,7 @@ If you are using Maven as your build tool, add the following dependency to your net.jacobpeterson.alpaca alpaca-java - 10.0.0 + 10.0.1 ``` diff --git a/build.gradle b/build.gradle index 6009b4a1..5dc0a489 100644 --- a/build.gradle +++ b/build.gradle @@ -22,7 +22,7 @@ plugins { final def projectGroup = "net.jacobpeterson.alpaca" final def projectArtifactID = "alpaca-java" -final def projectVersion = "10.0.0-SNAPSHOT" +final def projectVersion = "10.0.1-SNAPSHOT" group = projectGroup version = projectVersion From a937af46cf1396a217501f597659be7cc11648ed Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Sun, 14 Apr 2024 10:19:52 -0700 Subject: [PATCH 77/84] Fix README logo URL, remove unused variable --- README.md | 2 +- .../plugin/jsonschema2pojoadapted/JSONSchemaFileConfig.groovy | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index fdb5fc2d..449b8c23 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

Logo

+

Logo

GitHub Repository Maven Central diff --git a/buildSrc/src/main/groovy/net/jacobpeterson/alpaca/buildsrc/plugin/jsonschema2pojoadapted/JSONSchemaFileConfig.groovy b/buildSrc/src/main/groovy/net/jacobpeterson/alpaca/buildsrc/plugin/jsonschema2pojoadapted/JSONSchemaFileConfig.groovy index 698a30b9..d4dfd8fd 100644 --- a/buildSrc/src/main/groovy/net/jacobpeterson/alpaca/buildsrc/plugin/jsonschema2pojoadapted/JSONSchemaFileConfig.groovy +++ b/buildSrc/src/main/groovy/net/jacobpeterson/alpaca/buildsrc/plugin/jsonschema2pojoadapted/JSONSchemaFileConfig.groovy @@ -7,7 +7,6 @@ class JSONSchemaFileConfig { private File sourceFile private String targetPackage - private String outputFilePath /** * Instantiates a new {@link JSONSchemaFileConfig}. @@ -18,7 +17,6 @@ class JSONSchemaFileConfig { JSONSchemaFileConfig(File sourceFile, String targetPackage) { this.sourceFile = sourceFile this.targetPackage = targetPackage - this.outputFilePath = outputFilePath } File getSourceFile() { From 125c78003d17c3a434085838cc4a06985b3fc354 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Tue, 2 Jul 2024 19:44:37 -0700 Subject: [PATCH 78/84] Minor updates to README --- README.md | 10 +++------- src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java | 10 +--------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 449b8c23..a7a4efb9 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@

# Overview -This library is a Java client implementation of the [Alpaca](https://alpaca.markets) API. Alpaca lets you trade with algorithms, connect with apps, and build services all with a commission-free trading API for stocks, crypto, and options. This library uses the [Alpaca OpenAPI Specifications](https://docs.alpaca.markets/openapi) to generate clients for the REST API with the [OkHttp](https://square.github.io/okhttp/) library, but implements the websocket and SSE streaming interface using a custom implementation with the [OkHttp](https://square.github.io/okhttp/) library. This library is community developed and if you have any questions, please ask them on [Github Discussions](https://github.com/Petersoj/alpaca-java/discussions), the [Alpaca Slack #dev-alpaca-java channel](https://alpaca.markets/slack), or on the [Alpaca Forums](https://forum.alpaca.markets/). This library strives to provide the complete Alpaca API as a Java client implementation, so open a [new issue](https://github.com/Petersoj/alpaca-java/issues) or [new pull request](https://github.com/Petersoj/alpaca-java/pulls) if you find something missing. +This library is a Java client implementation of the [Alpaca](https://alpaca.markets) API. Alpaca lets you trade with algorithms, connect with apps, and build services all with a commission-free trading API for stocks, crypto, and options. This library uses the [Alpaca OpenAPI Specification](https://docs.alpaca.markets/openapi) to generate clients for the REST API with the [OkHttp](https://square.github.io/okhttp/) library, but implements the websocket and SSE streaming interface using a custom implementation with the [OkHttp](https://square.github.io/okhttp/) library. This library is community developed and if you have any questions, please ask them on [Github Discussions](https://github.com/Petersoj/alpaca-java/discussions), the [Alpaca Slack #dev-alpaca-java channel](https://alpaca.markets/slack), or on the [Alpaca Forums](https://forum.alpaca.markets/). This library strives to provide the complete Alpaca API as a Java client implementation, so open a [new issue](https://github.com/Petersoj/alpaca-java/issues) or [new pull request](https://github.com/Petersoj/alpaca-java/pulls) if you find something missing. Give this repository a star ⭐ if it helped you build a trading algorithm in Java! @@ -34,7 +34,7 @@ Note that you don't have to use the Maven Central artifacts. Instead, you can cl For logging, this library uses [SLF4j](http://www.slf4j.org/) which serves as an interface for various logging frameworks. This enables you to use whatever logging framework you would like. However, if you do not add a logging framework as a dependency in your project, the console will output a message stating that SLF4j is defaulting to a no-operation (NOP) logger implementation. To enable logging, add a logging framework of your choice as a dependency to your project such as [Logback](http://logback.qos.ch/), [Log4j 2](http://logging.apache.org/log4j/2.x/index.html), [SLF4j-simple](http://www.slf4j.org/manual.html), or [Apache Commons Logging](https://commons.apache.org/proper/commons-logging/). # `BigDecimal` vs. `Double` -It is generally considered bad practice to represent currency values in floating-point data types such as `float` or `double` because it can lead to [rounding errors](https://ta4j.github.io/ta4j-wiki/Num.html) and [precision loss](https://stackoverflow.com/a/3730040/4352701) in calculations. However, using floating-point data types can have significant performance benefits compared to using arbitrary-precision number data types, especially in a quantitative finance and algorithmic trading environment. Because of this, `alpaca-java` uses the `Double` data type (the `double` boxed type) when using the Market Data APIs and the `BigDecimal` data type when using the Trading or Broker APIs. The thinking behind this is that exact decimal quantities are important when placing real trades with real money, but less important when performing calculations of financial indicators. The best solution would be to use the [TA4j](https://github.com/ta4j/ta4j) `Num` interface so that you can decide what data type to use based on your use case, but that is on the [TODO list](#todo) for now. By the way, using `BigDecimal` wouldn't matter if Alpaca used floating-point data types in their internal systems or in their REST APIs, but the fact that they use `String` data types in some of the REST API JSON responses and their Trading and Broker OpenAPI specifications don't use the `double` data type, this leads me to believe that using `BigDecimal` does actually matter. +It is generally considered bad practice to represent monetary values using floating-point number data types such as `float` or `double` because it can lead to [rounding errors](https://ta4j.github.io/ta4j-wiki/Num.html) and [precision loss](https://stackoverflow.com/a/3730040/4352701). However, using floating-point number data types can have significant performance benefits compared to using fixed-point number data types (via `BigDecimal`), especially in a quantitative finance and algorithmic trading environment. Because of this, `alpaca-java` uses the `Double` data type (the `double` boxed type) when using the Market Data APIs and the `BigDecimal` data type when using the Trading or Broker APIs. The rationale behind this is that exact decimal quantities are important when placing real trades with real money, but less important when performing calculations through technical indicators. The best solution would be to use the [TA4j](https://github.com/ta4j/ta4j) `Num` interface so that you can decide what data type to use based on your use case, but that is on the [TODO list](#todo) for now. By the way, using `BigDecimal` wouldn't matter if Alpaca used floating-point data types in their internal systems, but the fact that they use `String` data types in some of the REST API JSON responses and their Trading and Broker OpenAPI specifications don't use the `double` data type, this leads me to believe that using `BigDecimal` does actually matter. # Examples Note that the examples below are not exhaustive. Refer to the [Javadoc](https://javadoc.io/doc/net.jacobpeterson.alpaca/alpaca-java) for all classes and method signatures. @@ -70,8 +70,6 @@ final BrokerAPIEndpointType endpointType = BrokerAPIEndpointType.SANDBOX; // or final AlpacaAPI alpacaAPI = new AlpacaAPI(brokerAPIKey, brokerAPISecret, endpointType); ``` -Note that this library uses [OkHttp](https://square.github.io/okhttp/) as its HTTP client library which creates background threads to service requests via a connection pool. These threads persist even if the main thread exits, so if you want to destroy these threads when you're done using [`AlpacaAPI`](src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java), use `alpacaAPI.closeOkHttpClient();`. - ## [`Trader API`](src/main/java/net/jacobpeterson/alpaca/rest/trader/AlpacaTraderAPI.java) The [`Trader API`](src/main/java/net/jacobpeterson/alpaca/rest/trader/AlpacaTraderAPI.java) is used for placing trades, updating account details, getting open positions, and more. Refer to the [Javadoc](https://javadoc.io/doc/net.jacobpeterson.alpaca/alpaca-java/latest/net/jacobpeterson/alpaca/rest/trader/AlpacaTraderAPI.html) for a list of all available method signatures. @@ -168,7 +166,6 @@ Thread.sleep(2000); // Close the SSE stream and the OkHttpClient to exit cleanly sseRequest.close(); -alpacaAPI.closeOkHttpClient(); ``` ## [`Updates Stream`](src/main/java/net/jacobpeterson/alpaca/websocket/updates/UpdatesWebsocket.java) @@ -205,7 +202,6 @@ Thread.sleep(5000); // Disconnect the 'updates' stream and exit cleanly alpacaAPI.updatesStream().disconnect(); -alpacaAPI.closeOkHttpClient(); ``` ## [`Market Data Stream`](src/main/java/net/jacobpeterson/alpaca/websocket/marketdata/MarketDataWebsocket.java) @@ -243,7 +239,6 @@ Thread.sleep(5000); // Disconnect the 'stock market data' stream and exit cleanly alpacaAPI.stockMarketDataStream().disconnect(); -alpacaAPI.closeOkHttpClient(); ``` # Building @@ -258,6 +253,7 @@ To install the built artifacts to your local Maven repository on your machine (t ``` # TODO +- Implement Options market data. - Implement better reconnect logic for Websockets and SSE streaming. - Implement Unit Testing for REST API and Websocket streaming (both live and mocked). - Use [TA4j](https://github.com/ta4j/ta4j) `Num` interface instead of `Double` or `BigDecimal` for number data types so that users can use either `Double` or `BigDecimal` for performance or precision in price data. diff --git a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java index c1c73096..a4da83cf 100644 --- a/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java +++ b/src/main/java/net/jacobpeterson/alpaca/AlpacaAPI.java @@ -171,7 +171,7 @@ public AlpacaAPI(String traderKeyID, String traderSecretKey, // Create default OkHttpClient instance if (okHttpClient == null) { - OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); + final OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); if (LOGGER.isDebugEnabled()) { final HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(LOGGER::debug); loggingInterceptor.setLevel(BODY); @@ -182,14 +182,6 @@ public AlpacaAPI(String traderKeyID, String traderSecretKey, this.okHttpClient = okHttpClient; } - /** - * Closes the {@link OkHttpClient}. - */ - public void closeOkHttpClient() { - okHttpClient.dispatcher().executorService().shutdown(); - okHttpClient.connectionPool().evictAll(); - } - /** * Gets the {@link OkHttpClient}. * From d985c839aaa6a645a67858deb6ad5be8d2435a7e Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Sun, 26 Jan 2025 18:09:53 -0800 Subject: [PATCH 79/84] Update OpenAPI spec IDs to v1.2, fix `EventsApiSSE` NTA events call --- build.gradle | 16 +++++----------- .../alpaca/rest/broker/events/EventsApiSSE.java | 12 +++++++----- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/build.gradle b/build.gradle index 5dc0a489..c3948da4 100644 --- a/build.gradle +++ b/build.gradle @@ -89,9 +89,9 @@ javadoc { // These IDs come from the URLs on this page: https://docs.alpaca.markets/openapi final def specIDsOfFileNames = Map.of( - "trader.json", "657760422129f9005cf4bd58", - "broker.json", "657771fc2471d20070183049", - "market-data.json", "6577704d1fb6b9007032bf39") + "trader.json", "673e19fc6dc876001ff495b2", + "broker.json", "673e1a03a0b2160060120aa0", + "market-data.json", "673e19f7fd59830026ac651b") final File specDownloadPath = new File(project.layout.buildDirectory.get().getAsFile(), "/download/openapi/") final File generatedClientLibrariesPath = new File(project.layout.buildDirectory.get().getAsFile(), @@ -196,7 +196,7 @@ final def fixOpenAPIGeneratedClientIssuesTask = tasks.register("fixOpenAPIGenera } doLast { - // TODO remove these manual fixes once OpenAPI fixes these issues + // TODO remove these manual and hacky fixes once OpenAPI fixes these issues final def sourceFilesPath = "src/main/java/net/jacobpeterson/alpaca/openapi" @@ -250,7 +250,7 @@ final def fixOpenAPIGeneratedClientIssuesTask = tasks.register("fixOpenAPIGenera } } - // Remove the necessary authentication header logic from the generate client's ApiClient + // Remove the necessary authentication header logic from the generate client's 'ApiClient' specIDsOfFileNames.keySet().stream().map { it.replace(".json", "") }.forEach { final def apiClientFile = Paths.get(generatedClientLibrariesPath.getPath(), it, sourceFilesPath, it.replace("-", ""), "ApiClient.java").toFile() @@ -293,20 +293,17 @@ publishing { artifactId = projectArtifactID version = projectVersion from(components["java"]) - pom { name = projectArtifactID description = "A Java API for Alpaca, the commission free, algo friendly, stock trading broker." url = "https://github.com/Petersoj/alpaca-java" inceptionYear = "2018" - licenses { license { name = "MIT License" url = "https://opensource.org/licenses/MIT" } } - developers { developer { id = "Petersoj" @@ -317,7 +314,6 @@ publishing { name = "main(String[] args)" } } - scm { url = "https://github.com/Petersoj/alpaca-java.git" connection = "scm:git:https://github.com/Petersoj/alpaca-java.git" @@ -326,7 +322,6 @@ publishing { } } } - repositories { maven { name = "OSSRH" @@ -339,7 +334,6 @@ publishing { } } } - signing { sign publishing.publications.mavenJava } diff --git a/src/main/java/net/jacobpeterson/alpaca/rest/broker/events/EventsApiSSE.java b/src/main/java/net/jacobpeterson/alpaca/rest/broker/events/EventsApiSSE.java index 65efee8f..2c18b5ff 100644 --- a/src/main/java/net/jacobpeterson/alpaca/rest/broker/events/EventsApiSSE.java +++ b/src/main/java/net/jacobpeterson/alpaca/rest/broker/events/EventsApiSSE.java @@ -6,6 +6,7 @@ import net.jacobpeterson.alpaca.openapi.broker.api.EventsApi; import net.jacobpeterson.alpaca.openapi.broker.model.AccountStatusEvent; import net.jacobpeterson.alpaca.openapi.broker.model.JournalStatusEvent; +import net.jacobpeterson.alpaca.openapi.broker.model.NonTradeActivityEvent; import net.jacobpeterson.alpaca.openapi.broker.model.SubscribeToAdminActionSSE200ResponseInner; import net.jacobpeterson.alpaca.openapi.broker.model.TradeUpdateEvent; import net.jacobpeterson.alpaca.openapi.broker.model.TradeUpdateEventV2; @@ -25,6 +26,7 @@ import java.lang.reflect.Type; import java.time.LocalDate; import java.time.OffsetDateTime; +import java.util.UUID; import static java.util.concurrent.TimeUnit.SECONDS; import static net.jacobpeterson.alpaca.openapi.broker.JSON.getGson; @@ -53,17 +55,17 @@ public EventsApiSSE(ApiClient apiClient) { } /** - * See {@link EventsApi#getV1EventsNta(String, String, String, Integer, Integer, String, String, Boolean)}. + * See {@link EventsApi#getV1EventsNta(String, String, String, Integer, Integer, String, String, Boolean, UUID)}. * * @return a {@link SSERequest} */ public SSERequest subscribeToNonTradingActivitiesEvents(String id, String since, String until, Integer sinceId, - Integer untilId, String sinceUlid, String untilUlid, Boolean includePreprocessing, - SSEListener sseListener) throws ApiException { // TODO OpenAPI response type is broken + Integer untilId, String sinceUlid, String untilUlid, Boolean includePreprocessing, UUID groupId, + SSEListener sseListener) throws ApiException { final Request request = eventsAPI.getV1EventsNtaCall(id, since, until, sinceId, untilId, sinceUlid, untilUlid, - includePreprocessing, null).request(); + includePreprocessing, groupId, null).request(); return new SSERequest(eventSourceFactory.newEventSource(request, createEventSourceListener(sseListener, - new TypeToken() {}.getType()))); // TODO OpenAPI response type is broken + new TypeToken() {}.getType()))); } /** From 1230b41d7b45924a10ffce12c3b9629d88415fc1 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:37:15 -0600 Subject: [PATCH 80/84] Remove `.editorconfig` --- .editorconfig | 1206 ------------------------------------------------- 1 file changed, 1206 deletions(-) delete mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index e8ed66b1..00000000 --- a/.editorconfig +++ /dev/null @@ -1,1206 +0,0 @@ -[*] -charset = utf-8 -end_of_line = lf -indent_size = 4 -indent_style = space -insert_final_newline = true -max_line_length = 120 -tab_width = 4 -ij_continuation_indent_size = 8 -ij_formatter_off_tag = @formatter:off -ij_formatter_on_tag = @formatter:on -ij_formatter_tags_enabled = true -ij_smart_tabs = false -ij_visual_guides = -ij_wrap_on_typing = false - -[*.css] -ij_css_align_closing_brace_with_properties = false -ij_css_blank_lines_around_nested_selector = 1 -ij_css_blank_lines_between_blocks = 1 -ij_css_block_comment_add_space = false -ij_css_brace_placement = end_of_line -ij_css_enforce_quotes_on_format = false -ij_css_hex_color_long_format = false -ij_css_hex_color_lower_case = false -ij_css_hex_color_short_format = false -ij_css_hex_color_upper_case = false -ij_css_keep_blank_lines_in_code = 2 -ij_css_keep_indents_on_empty_lines = false -ij_css_keep_single_line_blocks = false -ij_css_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow -ij_css_space_after_colon = true -ij_css_space_before_opening_brace = true -ij_css_use_double_quotes = true -ij_css_value_alignment = do_not_align - -[*.feature] -indent_size = 2 -ij_gherkin_keep_indents_on_empty_lines = false - -[*.java] -ij_java_align_consecutive_assignments = false -ij_java_align_consecutive_variable_declarations = false -ij_java_align_group_field_declarations = false -ij_java_align_multiline_annotation_parameters = false -ij_java_align_multiline_array_initializer_expression = false -ij_java_align_multiline_assignment = false -ij_java_align_multiline_binary_operation = false -ij_java_align_multiline_chained_methods = false -ij_java_align_multiline_deconstruction_list_components = false -ij_java_align_multiline_extends_list = false -ij_java_align_multiline_for = false -ij_java_align_multiline_method_parentheses = false -ij_java_align_multiline_parameters = false -ij_java_align_multiline_parameters_in_calls = false -ij_java_align_multiline_parenthesized_expression = false -ij_java_align_multiline_records = false -ij_java_align_multiline_resources = false -ij_java_align_multiline_ternary_operation = false -ij_java_align_multiline_text_blocks = false -ij_java_align_multiline_throws_list = false -ij_java_align_subsequent_simple_methods = false -ij_java_align_throws_keyword = false -ij_java_align_types_in_multi_catch = false -ij_java_annotation_parameter_wrap = normal -ij_java_array_initializer_new_line_after_left_brace = false -ij_java_array_initializer_right_brace_on_new_line = false -ij_java_array_initializer_wrap = normal -ij_java_assert_statement_colon_on_next_line = false -ij_java_assert_statement_wrap = normal -ij_java_assignment_wrap = normal -ij_java_binary_operation_sign_on_next_line = false -ij_java_binary_operation_wrap = normal -ij_java_blank_lines_after_anonymous_class_header = 0 -ij_java_blank_lines_after_class_header = 1 -ij_java_blank_lines_after_imports = 1 -ij_java_blank_lines_after_package = 1 -ij_java_blank_lines_around_class = 1 -ij_java_blank_lines_around_field = 0 -ij_java_blank_lines_around_field_in_interface = 0 -ij_java_blank_lines_around_initializer = 1 -ij_java_blank_lines_around_method = 1 -ij_java_blank_lines_around_method_in_interface = 1 -ij_java_blank_lines_before_class_end = 0 -ij_java_blank_lines_before_imports = 1 -ij_java_blank_lines_before_method_body = 0 -ij_java_blank_lines_before_package = 0 -ij_java_block_brace_style = end_of_line -ij_java_block_comment_add_space = false -ij_java_block_comment_at_first_column = true -ij_java_builder_methods = -ij_java_call_parameters_new_line_after_left_paren = false -ij_java_call_parameters_right_paren_on_new_line = false -ij_java_call_parameters_wrap = normal -ij_java_case_statement_on_separate_line = true -ij_java_catch_on_new_line = false -ij_java_class_annotation_wrap = normal -ij_java_class_brace_style = end_of_line -ij_java_class_count_to_use_import_on_demand = 999 -ij_java_class_names_in_javadoc = 3 -ij_java_deconstruction_list_wrap = normal -ij_java_do_not_indent_top_level_class_members = false -ij_java_do_not_wrap_after_single_annotation = false -ij_java_do_not_wrap_after_single_annotation_in_parameter = false -ij_java_do_while_brace_force = always -ij_java_doc_add_blank_line_after_description = true -ij_java_doc_add_blank_line_after_param_comments = true -ij_java_doc_add_blank_line_after_return = true -ij_java_doc_add_p_tag_on_empty_lines = true -ij_java_doc_align_exception_comments = true -ij_java_doc_align_param_comments = true -ij_java_doc_do_not_wrap_if_one_line = false -ij_java_doc_enable_formatting = true -ij_java_doc_enable_leading_asterisks = true -ij_java_doc_indent_on_continuation = false -ij_java_doc_keep_empty_lines = true -ij_java_doc_keep_empty_parameter_tag = true -ij_java_doc_keep_empty_return_tag = true -ij_java_doc_keep_empty_throws_tag = true -ij_java_doc_keep_invalid_tags = true -ij_java_doc_param_description_on_new_line = false -ij_java_doc_preserve_line_breaks = false -ij_java_doc_use_throws_not_exception_tag = true -ij_java_else_on_new_line = false -ij_java_entity_dd_prefix = -ij_java_entity_dd_suffix = EJB -ij_java_entity_eb_prefix = -ij_java_entity_eb_suffix = Bean -ij_java_entity_hi_prefix = -ij_java_entity_hi_suffix = Home -ij_java_entity_lhi_prefix = Local -ij_java_entity_lhi_suffix = Home -ij_java_entity_li_prefix = Local -ij_java_entity_li_suffix = -ij_java_entity_pk_class = java.lang.String -ij_java_entity_ri_prefix = -ij_java_entity_ri_suffix = -ij_java_entity_vo_prefix = -ij_java_entity_vo_suffix = VO -ij_java_enum_constants_wrap = split_into_lines -ij_java_extends_keyword_wrap = normal -ij_java_extends_list_wrap = normal -ij_java_field_annotation_wrap = normal -ij_java_field_name_prefix = -ij_java_field_name_suffix = -ij_java_filter_class_prefix = -ij_java_filter_class_suffix = -ij_java_filter_dd_prefix = -ij_java_filter_dd_suffix = -ij_java_finally_on_new_line = false -ij_java_for_brace_force = always -ij_java_for_statement_new_line_after_left_paren = false -ij_java_for_statement_right_paren_on_new_line = false -ij_java_for_statement_wrap = normal -ij_java_generate_final_locals = false -ij_java_generate_final_parameters = false -ij_java_if_brace_force = always -ij_java_imports_layout = *,|,javax.**,java.**,|,$* -ij_java_indent_case_from_switch = true -ij_java_insert_inner_class_imports = false -ij_java_insert_override_annotation = true -ij_java_keep_blank_lines_before_right_brace = 1 -ij_java_keep_blank_lines_between_package_declaration_and_header = 1 -ij_java_keep_blank_lines_in_code = 1 -ij_java_keep_blank_lines_in_declarations = 1 -ij_java_keep_builder_methods_indents = false -ij_java_keep_control_statement_in_one_line = true -ij_java_keep_first_column_comment = true -ij_java_keep_indents_on_empty_lines = false -ij_java_keep_line_breaks = true -ij_java_keep_multiple_expressions_in_one_line = true -ij_java_keep_simple_blocks_in_one_line = true -ij_java_keep_simple_classes_in_one_line = true -ij_java_keep_simple_lambdas_in_one_line = true -ij_java_keep_simple_methods_in_one_line = true -ij_java_label_indent_absolute = false -ij_java_label_indent_size = 0 -ij_java_lambda_brace_style = end_of_line -ij_java_layout_static_imports_separately = true -ij_java_line_comment_add_space = false -ij_java_line_comment_add_space_on_reformat = false -ij_java_line_comment_at_first_column = true -ij_java_listener_class_prefix = -ij_java_listener_class_suffix = -ij_java_local_variable_name_prefix = -ij_java_local_variable_name_suffix = -ij_java_message_dd_prefix = -ij_java_message_dd_suffix = EJB -ij_java_message_eb_prefix = -ij_java_message_eb_suffix = Bean -ij_java_method_annotation_wrap = normal -ij_java_method_brace_style = end_of_line -ij_java_method_call_chain_wrap = normal -ij_java_method_parameters_new_line_after_left_paren = false -ij_java_method_parameters_right_paren_on_new_line = false -ij_java_method_parameters_wrap = normal -ij_java_modifier_list_wrap = false -ij_java_multi_catch_types_wrap = normal -ij_java_names_count_to_use_import_on_demand = 999 -ij_java_new_line_after_lparen_in_annotation = false -ij_java_new_line_after_lparen_in_deconstruction_pattern = false -ij_java_new_line_after_lparen_in_record_header = false -ij_java_packages_to_use_import_on_demand = -ij_java_parameter_annotation_wrap = normal -ij_java_parameter_name_prefix = -ij_java_parameter_name_suffix = -ij_java_parentheses_expression_new_line_after_left_paren = false -ij_java_parentheses_expression_right_paren_on_new_line = false -ij_java_place_assignment_sign_on_next_line = false -ij_java_prefer_longer_names = true -ij_java_prefer_parameters_wrap = false -ij_java_record_components_wrap = normal -ij_java_repeat_annotations = -ij_java_repeat_synchronized = true -ij_java_replace_instanceof_and_cast = false -ij_java_replace_null_check = true -ij_java_replace_sum_lambda_with_method_ref = true -ij_java_resource_list_new_line_after_left_paren = false -ij_java_resource_list_right_paren_on_new_line = false -ij_java_resource_list_wrap = normal -ij_java_rparen_on_new_line_in_annotation = false -ij_java_rparen_on_new_line_in_deconstruction_pattern = false -ij_java_rparen_on_new_line_in_record_header = false -ij_java_servlet_class_prefix = -ij_java_servlet_class_suffix = -ij_java_servlet_dd_prefix = -ij_java_servlet_dd_suffix = -ij_java_session_dd_prefix = -ij_java_session_dd_suffix = EJB -ij_java_session_eb_prefix = -ij_java_session_eb_suffix = Bean -ij_java_session_hi_prefix = -ij_java_session_hi_suffix = Home -ij_java_session_lhi_prefix = Local -ij_java_session_lhi_suffix = Home -ij_java_session_li_prefix = Local -ij_java_session_li_suffix = -ij_java_session_ri_prefix = -ij_java_session_ri_suffix = -ij_java_session_si_prefix = -ij_java_session_si_suffix = Service -ij_java_space_after_closing_angle_bracket_in_type_argument = false -ij_java_space_after_colon = true -ij_java_space_after_comma = true -ij_java_space_after_comma_in_type_arguments = true -ij_java_space_after_for_semicolon = true -ij_java_space_after_quest = true -ij_java_space_after_type_cast = true -ij_java_space_before_annotation_array_initializer_left_brace = false -ij_java_space_before_annotation_parameter_list = false -ij_java_space_before_array_initializer_left_brace = false -ij_java_space_before_catch_keyword = true -ij_java_space_before_catch_left_brace = true -ij_java_space_before_catch_parentheses = true -ij_java_space_before_class_left_brace = true -ij_java_space_before_colon = true -ij_java_space_before_colon_in_foreach = true -ij_java_space_before_comma = false -ij_java_space_before_deconstruction_list = false -ij_java_space_before_do_left_brace = true -ij_java_space_before_else_keyword = true -ij_java_space_before_else_left_brace = true -ij_java_space_before_finally_keyword = true -ij_java_space_before_finally_left_brace = true -ij_java_space_before_for_left_brace = true -ij_java_space_before_for_parentheses = true -ij_java_space_before_for_semicolon = false -ij_java_space_before_if_left_brace = true -ij_java_space_before_if_parentheses = true -ij_java_space_before_method_call_parentheses = false -ij_java_space_before_method_left_brace = true -ij_java_space_before_method_parentheses = false -ij_java_space_before_opening_angle_bracket_in_type_parameter = false -ij_java_space_before_quest = true -ij_java_space_before_switch_left_brace = true -ij_java_space_before_switch_parentheses = true -ij_java_space_before_synchronized_left_brace = true -ij_java_space_before_synchronized_parentheses = true -ij_java_space_before_try_left_brace = true -ij_java_space_before_try_parentheses = true -ij_java_space_before_type_parameter_list = false -ij_java_space_before_while_keyword = true -ij_java_space_before_while_left_brace = true -ij_java_space_before_while_parentheses = true -ij_java_space_inside_one_line_enum_braces = false -ij_java_space_within_empty_array_initializer_braces = false -ij_java_space_within_empty_method_call_parentheses = false -ij_java_space_within_empty_method_parentheses = false -ij_java_spaces_around_additive_operators = true -ij_java_spaces_around_annotation_eq = true -ij_java_spaces_around_assignment_operators = true -ij_java_spaces_around_bitwise_operators = true -ij_java_spaces_around_equality_operators = true -ij_java_spaces_around_lambda_arrow = true -ij_java_spaces_around_logical_operators = true -ij_java_spaces_around_method_ref_dbl_colon = false -ij_java_spaces_around_multiplicative_operators = true -ij_java_spaces_around_relational_operators = true -ij_java_spaces_around_shift_operators = true -ij_java_spaces_around_type_bounds_in_type_parameters = true -ij_java_spaces_around_unary_operator = false -ij_java_spaces_within_angle_brackets = false -ij_java_spaces_within_annotation_parentheses = false -ij_java_spaces_within_array_initializer_braces = false -ij_java_spaces_within_braces = false -ij_java_spaces_within_brackets = false -ij_java_spaces_within_cast_parentheses = false -ij_java_spaces_within_catch_parentheses = false -ij_java_spaces_within_deconstruction_list = false -ij_java_spaces_within_for_parentheses = false -ij_java_spaces_within_if_parentheses = false -ij_java_spaces_within_method_call_parentheses = false -ij_java_spaces_within_method_parentheses = false -ij_java_spaces_within_parentheses = false -ij_java_spaces_within_record_header = false -ij_java_spaces_within_switch_parentheses = false -ij_java_spaces_within_synchronized_parentheses = false -ij_java_spaces_within_try_parentheses = false -ij_java_spaces_within_while_parentheses = false -ij_java_special_else_if_treatment = true -ij_java_static_field_name_prefix = -ij_java_static_field_name_suffix = -ij_java_subclass_name_prefix = -ij_java_subclass_name_suffix = Impl -ij_java_ternary_operation_signs_on_next_line = false -ij_java_ternary_operation_wrap = normal -ij_java_test_name_prefix = -ij_java_test_name_suffix = Test -ij_java_throws_keyword_wrap = normal -ij_java_throws_list_wrap = normal -ij_java_use_external_annotations = false -ij_java_use_fq_class_names = false -ij_java_use_relative_indents = false -ij_java_use_single_class_imports = true -ij_java_variable_annotation_wrap = normal -ij_java_visibility = public -ij_java_while_brace_force = always -ij_java_while_on_new_line = false -ij_java_wrap_comments = true -ij_java_wrap_first_method_in_call_chain = false -ij_java_wrap_long_lines = false - -[*.less] -indent_size = 2 -ij_less_align_closing_brace_with_properties = false -ij_less_blank_lines_around_nested_selector = 1 -ij_less_blank_lines_between_blocks = 1 -ij_less_block_comment_add_space = false -ij_less_brace_placement = 0 -ij_less_enforce_quotes_on_format = false -ij_less_hex_color_long_format = false -ij_less_hex_color_lower_case = false -ij_less_hex_color_short_format = false -ij_less_hex_color_upper_case = false -ij_less_keep_blank_lines_in_code = 2 -ij_less_keep_indents_on_empty_lines = false -ij_less_keep_single_line_blocks = false -ij_less_line_comment_add_space = false -ij_less_line_comment_at_first_column = false -ij_less_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow -ij_less_space_after_colon = true -ij_less_space_before_opening_brace = true -ij_less_use_double_quotes = true -ij_less_value_alignment = 0 - -[*.proto] -indent_size = 2 -tab_width = 2 -ij_continuation_indent_size = 4 -ij_protobuf_keep_blank_lines_in_code = 2 -ij_protobuf_keep_indents_on_empty_lines = false -ij_protobuf_keep_line_breaks = true -ij_protobuf_space_after_comma = true -ij_protobuf_space_before_comma = false -ij_protobuf_spaces_around_assignment_operators = true -ij_protobuf_spaces_within_braces = false -ij_protobuf_spaces_within_brackets = false - -[*.sass] -indent_size = 2 -ij_sass_align_closing_brace_with_properties = false -ij_sass_blank_lines_around_nested_selector = 1 -ij_sass_blank_lines_between_blocks = 1 -ij_sass_brace_placement = 0 -ij_sass_enforce_quotes_on_format = false -ij_sass_hex_color_long_format = false -ij_sass_hex_color_lower_case = false -ij_sass_hex_color_short_format = false -ij_sass_hex_color_upper_case = false -ij_sass_keep_blank_lines_in_code = 2 -ij_sass_keep_indents_on_empty_lines = false -ij_sass_keep_single_line_blocks = false -ij_sass_line_comment_add_space = false -ij_sass_line_comment_at_first_column = false -ij_sass_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow -ij_sass_space_after_colon = true -ij_sass_space_before_opening_brace = true -ij_sass_use_double_quotes = true -ij_sass_value_alignment = 0 - -[*.scss] -indent_size = 2 -ij_scss_align_closing_brace_with_properties = false -ij_scss_blank_lines_around_nested_selector = 1 -ij_scss_blank_lines_between_blocks = 1 -ij_scss_block_comment_add_space = false -ij_scss_brace_placement = 0 -ij_scss_enforce_quotes_on_format = false -ij_scss_hex_color_long_format = false -ij_scss_hex_color_lower_case = false -ij_scss_hex_color_short_format = false -ij_scss_hex_color_upper_case = false -ij_scss_keep_blank_lines_in_code = 2 -ij_scss_keep_indents_on_empty_lines = false -ij_scss_keep_single_line_blocks = false -ij_scss_line_comment_add_space = false -ij_scss_line_comment_at_first_column = false -ij_scss_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow -ij_scss_space_after_colon = true -ij_scss_space_before_opening_brace = true -ij_scss_use_double_quotes = true -ij_scss_value_alignment = 0 - -[*.vue] -indent_size = 2 -tab_width = 2 -ij_continuation_indent_size = 4 -ij_vue_indent_children_of_top_level = template -ij_vue_interpolation_new_line_after_start_delimiter = true -ij_vue_interpolation_new_line_before_end_delimiter = true -ij_vue_interpolation_wrap = off -ij_vue_keep_indents_on_empty_lines = false -ij_vue_spaces_within_interpolation_expressions = true - -[.editorconfig] -ij_editorconfig_align_group_field_declarations = false -ij_editorconfig_space_after_colon = false -ij_editorconfig_space_after_comma = true -ij_editorconfig_space_before_colon = false -ij_editorconfig_space_before_comma = false -ij_editorconfig_spaces_around_assignment_operators = true - -[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.pom,*.rng,*.tld,*.wadl,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] -ij_xml_align_attributes = true -ij_xml_align_text = false -ij_xml_attribute_wrap = normal -ij_xml_block_comment_add_space = false -ij_xml_block_comment_at_first_column = true -ij_xml_keep_blank_lines = 2 -ij_xml_keep_indents_on_empty_lines = false -ij_xml_keep_line_breaks = true -ij_xml_keep_line_breaks_in_text = true -ij_xml_keep_whitespaces = false -ij_xml_keep_whitespaces_around_cdata = preserve -ij_xml_keep_whitespaces_inside_cdata = false -ij_xml_line_comment_at_first_column = true -ij_xml_space_after_tag_name = false -ij_xml_space_around_equals_in_attribute = false -ij_xml_space_inside_empty_tag = false -ij_xml_text_wrap = normal -ij_xml_use_custom_settings = false - -[{*.ats,*.cts,*.mts,*.ts}] -ij_typescript_align_imports = false -ij_typescript_align_multiline_array_initializer_expression = false -ij_typescript_align_multiline_binary_operation = false -ij_typescript_align_multiline_chained_methods = false -ij_typescript_align_multiline_extends_list = false -ij_typescript_align_multiline_for = false -ij_typescript_align_multiline_parameters = false -ij_typescript_align_multiline_parameters_in_calls = false -ij_typescript_align_multiline_ternary_operation = false -ij_typescript_align_object_properties = 0 -ij_typescript_align_union_types = false -ij_typescript_align_var_statements = 0 -ij_typescript_array_initializer_new_line_after_left_brace = false -ij_typescript_array_initializer_right_brace_on_new_line = false -ij_typescript_array_initializer_wrap = normal -ij_typescript_assignment_wrap = normal -ij_typescript_binary_operation_sign_on_next_line = false -ij_typescript_binary_operation_wrap = normal -ij_typescript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** -ij_typescript_blank_lines_after_imports = 1 -ij_typescript_blank_lines_around_class = 1 -ij_typescript_blank_lines_around_field = 0 -ij_typescript_blank_lines_around_field_in_interface = 0 -ij_typescript_blank_lines_around_function = 1 -ij_typescript_blank_lines_around_method = 1 -ij_typescript_blank_lines_around_method_in_interface = 1 -ij_typescript_block_brace_style = end_of_line -ij_typescript_block_comment_add_space = false -ij_typescript_block_comment_at_first_column = true -ij_typescript_call_parameters_new_line_after_left_paren = false -ij_typescript_call_parameters_right_paren_on_new_line = false -ij_typescript_call_parameters_wrap = normal -ij_typescript_catch_on_new_line = false -ij_typescript_chained_call_dot_on_new_line = true -ij_typescript_class_brace_style = end_of_line -ij_typescript_comma_on_new_line = false -ij_typescript_do_while_brace_force = always -ij_typescript_else_on_new_line = false -ij_typescript_enforce_trailing_comma = remove -ij_typescript_enum_constants_wrap = split_into_lines -ij_typescript_extends_keyword_wrap = normal -ij_typescript_extends_list_wrap = normal -ij_typescript_field_prefix = _ -ij_typescript_file_name_style = relaxed -ij_typescript_finally_on_new_line = false -ij_typescript_for_brace_force = always -ij_typescript_for_statement_new_line_after_left_paren = false -ij_typescript_for_statement_right_paren_on_new_line = false -ij_typescript_for_statement_wrap = normal -ij_typescript_force_quote_style = true -ij_typescript_force_semicolon_style = true -ij_typescript_function_expression_brace_style = end_of_line -ij_typescript_if_brace_force = always -ij_typescript_import_merge_members = global -ij_typescript_import_prefer_absolute_path = global -ij_typescript_import_sort_members = true -ij_typescript_import_sort_module_name = false -ij_typescript_import_use_node_resolution = true -ij_typescript_imports_wrap = on_every_item -ij_typescript_indent_case_from_switch = true -ij_typescript_indent_chained_calls = true -ij_typescript_indent_package_children = 0 -ij_typescript_jsdoc_include_types = false -ij_typescript_jsx_attribute_value = braces -ij_typescript_keep_blank_lines_in_code = 1 -ij_typescript_keep_first_column_comment = true -ij_typescript_keep_indents_on_empty_lines = false -ij_typescript_keep_line_breaks = true -ij_typescript_keep_simple_blocks_in_one_line = true -ij_typescript_keep_simple_methods_in_one_line = true -ij_typescript_line_comment_add_space = true -ij_typescript_line_comment_at_first_column = false -ij_typescript_method_brace_style = end_of_line -ij_typescript_method_call_chain_wrap = normal -ij_typescript_method_parameters_new_line_after_left_paren = false -ij_typescript_method_parameters_right_paren_on_new_line = false -ij_typescript_method_parameters_wrap = normal -ij_typescript_object_literal_wrap = on_every_item -ij_typescript_object_types_wrap = on_every_item -ij_typescript_parentheses_expression_new_line_after_left_paren = false -ij_typescript_parentheses_expression_right_paren_on_new_line = false -ij_typescript_place_assignment_sign_on_next_line = false -ij_typescript_prefer_as_type_cast = false -ij_typescript_prefer_explicit_types_function_expression_returns = false -ij_typescript_prefer_explicit_types_function_returns = false -ij_typescript_prefer_explicit_types_vars_fields = false -ij_typescript_prefer_parameters_wrap = false -ij_typescript_property_prefix = -ij_typescript_reformat_c_style_comments = false -ij_typescript_space_after_colon = true -ij_typescript_space_after_comma = true -ij_typescript_space_after_dots_in_rest_parameter = false -ij_typescript_space_after_generator_mult = true -ij_typescript_space_after_property_colon = true -ij_typescript_space_after_quest = true -ij_typescript_space_after_type_colon = true -ij_typescript_space_after_unary_not = false -ij_typescript_space_before_async_arrow_lparen = true -ij_typescript_space_before_catch_keyword = true -ij_typescript_space_before_catch_left_brace = true -ij_typescript_space_before_catch_parentheses = true -ij_typescript_space_before_class_lbrace = true -ij_typescript_space_before_class_left_brace = true -ij_typescript_space_before_colon = true -ij_typescript_space_before_comma = false -ij_typescript_space_before_do_left_brace = true -ij_typescript_space_before_else_keyword = true -ij_typescript_space_before_else_left_brace = true -ij_typescript_space_before_finally_keyword = true -ij_typescript_space_before_finally_left_brace = true -ij_typescript_space_before_for_left_brace = true -ij_typescript_space_before_for_parentheses = true -ij_typescript_space_before_for_semicolon = false -ij_typescript_space_before_function_left_parenth = true -ij_typescript_space_before_generator_mult = false -ij_typescript_space_before_if_left_brace = true -ij_typescript_space_before_if_parentheses = true -ij_typescript_space_before_method_call_parentheses = false -ij_typescript_space_before_method_left_brace = true -ij_typescript_space_before_method_parentheses = false -ij_typescript_space_before_property_colon = false -ij_typescript_space_before_quest = true -ij_typescript_space_before_switch_left_brace = true -ij_typescript_space_before_switch_parentheses = true -ij_typescript_space_before_try_left_brace = true -ij_typescript_space_before_type_colon = false -ij_typescript_space_before_unary_not = false -ij_typescript_space_before_while_keyword = true -ij_typescript_space_before_while_left_brace = true -ij_typescript_space_before_while_parentheses = true -ij_typescript_spaces_around_additive_operators = true -ij_typescript_spaces_around_arrow_function_operator = true -ij_typescript_spaces_around_assignment_operators = true -ij_typescript_spaces_around_bitwise_operators = true -ij_typescript_spaces_around_equality_operators = true -ij_typescript_spaces_around_logical_operators = true -ij_typescript_spaces_around_multiplicative_operators = true -ij_typescript_spaces_around_relational_operators = true -ij_typescript_spaces_around_shift_operators = true -ij_typescript_spaces_around_unary_operator = false -ij_typescript_spaces_within_array_initializer_brackets = false -ij_typescript_spaces_within_brackets = false -ij_typescript_spaces_within_catch_parentheses = false -ij_typescript_spaces_within_for_parentheses = false -ij_typescript_spaces_within_if_parentheses = false -ij_typescript_spaces_within_imports = false -ij_typescript_spaces_within_interpolation_expressions = false -ij_typescript_spaces_within_method_call_parentheses = false -ij_typescript_spaces_within_method_parentheses = false -ij_typescript_spaces_within_object_literal_braces = false -ij_typescript_spaces_within_object_type_braces = true -ij_typescript_spaces_within_parentheses = false -ij_typescript_spaces_within_switch_parentheses = false -ij_typescript_spaces_within_type_assertion = false -ij_typescript_spaces_within_union_types = true -ij_typescript_spaces_within_while_parentheses = false -ij_typescript_special_else_if_treatment = true -ij_typescript_ternary_operation_signs_on_next_line = false -ij_typescript_ternary_operation_wrap = normal -ij_typescript_union_types_wrap = on_every_item -ij_typescript_use_chained_calls_group_indents = false -ij_typescript_use_double_quotes = true -ij_typescript_use_explicit_js_extension = auto -ij_typescript_use_path_mapping = always -ij_typescript_use_public_modifier = false -ij_typescript_use_semicolon_after_statement = true -ij_typescript_var_declaration_wrap = normal -ij_typescript_while_brace_force = always -ij_typescript_while_on_new_line = false -ij_typescript_wrap_comments = true - -[{*.bash,*.sh,*.zsh}] -indent_size = 2 -tab_width = 2 -ij_shell_binary_ops_start_line = false -ij_shell_keep_column_alignment_padding = false -ij_shell_minify_program = false -ij_shell_redirect_followed_by_space = false -ij_shell_switch_cases_indented = false -ij_shell_use_unix_line_separator = true - -[{*.cjs,*.js}] -ij_javascript_align_imports = false -ij_javascript_align_multiline_array_initializer_expression = false -ij_javascript_align_multiline_binary_operation = false -ij_javascript_align_multiline_chained_methods = false -ij_javascript_align_multiline_extends_list = false -ij_javascript_align_multiline_for = false -ij_javascript_align_multiline_parameters = false -ij_javascript_align_multiline_parameters_in_calls = false -ij_javascript_align_multiline_ternary_operation = false -ij_javascript_align_object_properties = 0 -ij_javascript_align_union_types = false -ij_javascript_align_var_statements = 0 -ij_javascript_array_initializer_new_line_after_left_brace = false -ij_javascript_array_initializer_right_brace_on_new_line = false -ij_javascript_array_initializer_wrap = normal -ij_javascript_assignment_wrap = normal -ij_javascript_binary_operation_sign_on_next_line = false -ij_javascript_binary_operation_wrap = normal -ij_javascript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** -ij_javascript_blank_lines_after_imports = 1 -ij_javascript_blank_lines_around_class = 1 -ij_javascript_blank_lines_around_field = 0 -ij_javascript_blank_lines_around_function = 1 -ij_javascript_blank_lines_around_method = 1 -ij_javascript_block_brace_style = end_of_line -ij_javascript_block_comment_add_space = false -ij_javascript_block_comment_at_first_column = true -ij_javascript_call_parameters_new_line_after_left_paren = false -ij_javascript_call_parameters_right_paren_on_new_line = false -ij_javascript_call_parameters_wrap = normal -ij_javascript_catch_on_new_line = false -ij_javascript_chained_call_dot_on_new_line = true -ij_javascript_class_brace_style = end_of_line -ij_javascript_comma_on_new_line = false -ij_javascript_do_while_brace_force = always -ij_javascript_else_on_new_line = false -ij_javascript_enforce_trailing_comma = remove -ij_javascript_extends_keyword_wrap = normal -ij_javascript_extends_list_wrap = normal -ij_javascript_field_prefix = _ -ij_javascript_file_name_style = relaxed -ij_javascript_finally_on_new_line = false -ij_javascript_for_brace_force = always -ij_javascript_for_statement_new_line_after_left_paren = false -ij_javascript_for_statement_right_paren_on_new_line = false -ij_javascript_for_statement_wrap = normal -ij_javascript_force_quote_style = true -ij_javascript_force_semicolon_style = true -ij_javascript_function_expression_brace_style = end_of_line -ij_javascript_if_brace_force = always -ij_javascript_import_merge_members = global -ij_javascript_import_prefer_absolute_path = global -ij_javascript_import_sort_members = true -ij_javascript_import_sort_module_name = false -ij_javascript_import_use_node_resolution = true -ij_javascript_imports_wrap = normal -ij_javascript_indent_case_from_switch = true -ij_javascript_indent_chained_calls = true -ij_javascript_indent_package_children = 0 -ij_javascript_jsx_attribute_value = braces -ij_javascript_keep_blank_lines_in_code = 1 -ij_javascript_keep_first_column_comment = true -ij_javascript_keep_indents_on_empty_lines = false -ij_javascript_keep_line_breaks = true -ij_javascript_keep_simple_blocks_in_one_line = true -ij_javascript_keep_simple_methods_in_one_line = true -ij_javascript_line_comment_add_space = true -ij_javascript_line_comment_at_first_column = false -ij_javascript_method_brace_style = end_of_line -ij_javascript_method_call_chain_wrap = normal -ij_javascript_method_parameters_new_line_after_left_paren = false -ij_javascript_method_parameters_right_paren_on_new_line = false -ij_javascript_method_parameters_wrap = normal -ij_javascript_object_literal_wrap = on_every_item -ij_javascript_object_types_wrap = on_every_item -ij_javascript_parentheses_expression_new_line_after_left_paren = false -ij_javascript_parentheses_expression_right_paren_on_new_line = false -ij_javascript_place_assignment_sign_on_next_line = false -ij_javascript_prefer_as_type_cast = false -ij_javascript_prefer_explicit_types_function_expression_returns = false -ij_javascript_prefer_explicit_types_function_returns = false -ij_javascript_prefer_explicit_types_vars_fields = false -ij_javascript_prefer_parameters_wrap = false -ij_javascript_property_prefix = -ij_javascript_reformat_c_style_comments = false -ij_javascript_space_after_colon = true -ij_javascript_space_after_comma = true -ij_javascript_space_after_dots_in_rest_parameter = false -ij_javascript_space_after_generator_mult = true -ij_javascript_space_after_property_colon = true -ij_javascript_space_after_quest = true -ij_javascript_space_after_type_colon = true -ij_javascript_space_after_unary_not = false -ij_javascript_space_before_async_arrow_lparen = true -ij_javascript_space_before_catch_keyword = true -ij_javascript_space_before_catch_left_brace = true -ij_javascript_space_before_catch_parentheses = true -ij_javascript_space_before_class_lbrace = true -ij_javascript_space_before_class_left_brace = true -ij_javascript_space_before_colon = true -ij_javascript_space_before_comma = false -ij_javascript_space_before_do_left_brace = true -ij_javascript_space_before_else_keyword = true -ij_javascript_space_before_else_left_brace = true -ij_javascript_space_before_finally_keyword = true -ij_javascript_space_before_finally_left_brace = true -ij_javascript_space_before_for_left_brace = true -ij_javascript_space_before_for_parentheses = true -ij_javascript_space_before_for_semicolon = false -ij_javascript_space_before_function_left_parenth = true -ij_javascript_space_before_generator_mult = false -ij_javascript_space_before_if_left_brace = true -ij_javascript_space_before_if_parentheses = true -ij_javascript_space_before_method_call_parentheses = false -ij_javascript_space_before_method_left_brace = true -ij_javascript_space_before_method_parentheses = false -ij_javascript_space_before_property_colon = false -ij_javascript_space_before_quest = true -ij_javascript_space_before_switch_left_brace = true -ij_javascript_space_before_switch_parentheses = true -ij_javascript_space_before_try_left_brace = true -ij_javascript_space_before_type_colon = false -ij_javascript_space_before_unary_not = false -ij_javascript_space_before_while_keyword = true -ij_javascript_space_before_while_left_brace = true -ij_javascript_space_before_while_parentheses = true -ij_javascript_spaces_around_additive_operators = true -ij_javascript_spaces_around_arrow_function_operator = true -ij_javascript_spaces_around_assignment_operators = true -ij_javascript_spaces_around_bitwise_operators = true -ij_javascript_spaces_around_equality_operators = true -ij_javascript_spaces_around_logical_operators = true -ij_javascript_spaces_around_multiplicative_operators = true -ij_javascript_spaces_around_relational_operators = true -ij_javascript_spaces_around_shift_operators = true -ij_javascript_spaces_around_unary_operator = false -ij_javascript_spaces_within_array_initializer_brackets = false -ij_javascript_spaces_within_brackets = false -ij_javascript_spaces_within_catch_parentheses = false -ij_javascript_spaces_within_for_parentheses = false -ij_javascript_spaces_within_if_parentheses = false -ij_javascript_spaces_within_imports = false -ij_javascript_spaces_within_interpolation_expressions = false -ij_javascript_spaces_within_method_call_parentheses = false -ij_javascript_spaces_within_method_parentheses = false -ij_javascript_spaces_within_object_literal_braces = false -ij_javascript_spaces_within_object_type_braces = true -ij_javascript_spaces_within_parentheses = false -ij_javascript_spaces_within_switch_parentheses = false -ij_javascript_spaces_within_type_assertion = false -ij_javascript_spaces_within_union_types = true -ij_javascript_spaces_within_while_parentheses = false -ij_javascript_special_else_if_treatment = true -ij_javascript_ternary_operation_signs_on_next_line = false -ij_javascript_ternary_operation_wrap = normal -ij_javascript_union_types_wrap = on_every_item -ij_javascript_use_chained_calls_group_indents = false -ij_javascript_use_double_quotes = true -ij_javascript_use_explicit_js_extension = auto -ij_javascript_use_path_mapping = always -ij_javascript_use_public_modifier = false -ij_javascript_use_semicolon_after_statement = true -ij_javascript_var_declaration_wrap = normal -ij_javascript_while_brace_force = always -ij_javascript_while_on_new_line = false -ij_javascript_wrap_comments = true - -[{*.ft,*.vm,*.vsl}] -ij_vtl_keep_indents_on_empty_lines = false - -[{*.gant,*.groovy,*.gy}] -ij_groovy_align_group_field_declarations = false -ij_groovy_align_multiline_array_initializer_expression = false -ij_groovy_align_multiline_assignment = false -ij_groovy_align_multiline_binary_operation = false -ij_groovy_align_multiline_chained_methods = false -ij_groovy_align_multiline_extends_list = false -ij_groovy_align_multiline_for = false -ij_groovy_align_multiline_list_or_map = false -ij_groovy_align_multiline_method_parentheses = false -ij_groovy_align_multiline_parameters = false -ij_groovy_align_multiline_parameters_in_calls = false -ij_groovy_align_multiline_resources = false -ij_groovy_align_multiline_ternary_operation = false -ij_groovy_align_multiline_throws_list = false -ij_groovy_align_named_args_in_map = false -ij_groovy_align_throws_keyword = false -ij_groovy_array_initializer_new_line_after_left_brace = false -ij_groovy_array_initializer_right_brace_on_new_line = false -ij_groovy_array_initializer_wrap = normal -ij_groovy_assert_statement_wrap = normal -ij_groovy_assignment_wrap = normal -ij_groovy_binary_operation_wrap = normal -ij_groovy_blank_lines_after_class_header = 0 -ij_groovy_blank_lines_after_imports = 1 -ij_groovy_blank_lines_after_package = 1 -ij_groovy_blank_lines_around_class = 1 -ij_groovy_blank_lines_around_field = 0 -ij_groovy_blank_lines_around_field_in_interface = 0 -ij_groovy_blank_lines_around_method = 1 -ij_groovy_blank_lines_around_method_in_interface = 1 -ij_groovy_blank_lines_before_imports = 1 -ij_groovy_blank_lines_before_method_body = 0 -ij_groovy_blank_lines_before_package = 0 -ij_groovy_block_brace_style = end_of_line -ij_groovy_block_comment_add_space = false -ij_groovy_block_comment_at_first_column = true -ij_groovy_call_parameters_new_line_after_left_paren = false -ij_groovy_call_parameters_right_paren_on_new_line = false -ij_groovy_call_parameters_wrap = normal -ij_groovy_catch_on_new_line = false -ij_groovy_class_annotation_wrap = split_into_lines -ij_groovy_class_brace_style = end_of_line -ij_groovy_class_count_to_use_import_on_demand = 999 -ij_groovy_do_while_brace_force = always -ij_groovy_else_on_new_line = false -ij_groovy_enable_groovydoc_formatting = true -ij_groovy_enum_constants_wrap = split_into_lines -ij_groovy_extends_keyword_wrap = normal -ij_groovy_extends_list_wrap = normal -ij_groovy_field_annotation_wrap = split_into_lines -ij_groovy_finally_on_new_line = false -ij_groovy_for_brace_force = always -ij_groovy_for_statement_new_line_after_left_paren = false -ij_groovy_for_statement_right_paren_on_new_line = false -ij_groovy_for_statement_wrap = normal -ij_groovy_ginq_general_clause_wrap_policy = 2 -ij_groovy_ginq_having_wrap_policy = 1 -ij_groovy_ginq_indent_having_clause = true -ij_groovy_ginq_indent_on_clause = true -ij_groovy_ginq_on_wrap_policy = 1 -ij_groovy_ginq_space_after_keyword = true -ij_groovy_if_brace_force = always -ij_groovy_import_annotation_wrap = 2 -ij_groovy_imports_layout = *,|,javax.**,java.**,|,$* -ij_groovy_indent_case_from_switch = true -ij_groovy_indent_label_blocks = true -ij_groovy_insert_inner_class_imports = false -ij_groovy_keep_blank_lines_before_right_brace = 1 -ij_groovy_keep_blank_lines_in_code = 1 -ij_groovy_keep_blank_lines_in_declarations = 1 -ij_groovy_keep_control_statement_in_one_line = true -ij_groovy_keep_first_column_comment = true -ij_groovy_keep_indents_on_empty_lines = false -ij_groovy_keep_line_breaks = true -ij_groovy_keep_multiple_expressions_in_one_line = true -ij_groovy_keep_simple_blocks_in_one_line = true -ij_groovy_keep_simple_classes_in_one_line = true -ij_groovy_keep_simple_lambdas_in_one_line = true -ij_groovy_keep_simple_methods_in_one_line = true -ij_groovy_label_indent_absolute = false -ij_groovy_label_indent_size = 0 -ij_groovy_lambda_brace_style = end_of_line -ij_groovy_layout_static_imports_separately = true -ij_groovy_line_comment_add_space = false -ij_groovy_line_comment_add_space_on_reformat = false -ij_groovy_line_comment_at_first_column = true -ij_groovy_method_annotation_wrap = split_into_lines -ij_groovy_method_brace_style = end_of_line -ij_groovy_method_call_chain_wrap = normal -ij_groovy_method_parameters_new_line_after_left_paren = false -ij_groovy_method_parameters_right_paren_on_new_line = false -ij_groovy_method_parameters_wrap = normal -ij_groovy_modifier_list_wrap = false -ij_groovy_names_count_to_use_import_on_demand = 999 -ij_groovy_packages_to_use_import_on_demand = * -ij_groovy_parameter_annotation_wrap = normal -ij_groovy_parentheses_expression_new_line_after_left_paren = false -ij_groovy_parentheses_expression_right_paren_on_new_line = false -ij_groovy_prefer_parameters_wrap = false -ij_groovy_resource_list_new_line_after_left_paren = false -ij_groovy_resource_list_right_paren_on_new_line = false -ij_groovy_resource_list_wrap = normal -ij_groovy_space_after_assert_separator = true -ij_groovy_space_after_colon = true -ij_groovy_space_after_comma = true -ij_groovy_space_after_comma_in_type_arguments = true -ij_groovy_space_after_for_semicolon = true -ij_groovy_space_after_quest = true -ij_groovy_space_after_type_cast = true -ij_groovy_space_before_annotation_parameter_list = false -ij_groovy_space_before_array_initializer_left_brace = false -ij_groovy_space_before_assert_separator = false -ij_groovy_space_before_catch_keyword = true -ij_groovy_space_before_catch_left_brace = true -ij_groovy_space_before_catch_parentheses = true -ij_groovy_space_before_class_left_brace = true -ij_groovy_space_before_closure_left_brace = true -ij_groovy_space_before_colon = true -ij_groovy_space_before_comma = false -ij_groovy_space_before_do_left_brace = true -ij_groovy_space_before_else_keyword = true -ij_groovy_space_before_else_left_brace = true -ij_groovy_space_before_finally_keyword = true -ij_groovy_space_before_finally_left_brace = true -ij_groovy_space_before_for_left_brace = true -ij_groovy_space_before_for_parentheses = true -ij_groovy_space_before_for_semicolon = false -ij_groovy_space_before_if_left_brace = true -ij_groovy_space_before_if_parentheses = true -ij_groovy_space_before_method_call_parentheses = false -ij_groovy_space_before_method_left_brace = true -ij_groovy_space_before_method_parentheses = false -ij_groovy_space_before_quest = true -ij_groovy_space_before_record_parentheses = false -ij_groovy_space_before_switch_left_brace = true -ij_groovy_space_before_switch_parentheses = true -ij_groovy_space_before_synchronized_left_brace = true -ij_groovy_space_before_synchronized_parentheses = true -ij_groovy_space_before_try_left_brace = true -ij_groovy_space_before_try_parentheses = true -ij_groovy_space_before_while_keyword = true -ij_groovy_space_before_while_left_brace = true -ij_groovy_space_before_while_parentheses = true -ij_groovy_space_in_named_argument = true -ij_groovy_space_in_named_argument_before_colon = false -ij_groovy_space_within_empty_array_initializer_braces = false -ij_groovy_space_within_empty_method_call_parentheses = false -ij_groovy_spaces_around_additive_operators = true -ij_groovy_spaces_around_assignment_operators = true -ij_groovy_spaces_around_bitwise_operators = true -ij_groovy_spaces_around_equality_operators = true -ij_groovy_spaces_around_lambda_arrow = true -ij_groovy_spaces_around_logical_operators = true -ij_groovy_spaces_around_multiplicative_operators = true -ij_groovy_spaces_around_regex_operators = true -ij_groovy_spaces_around_relational_operators = true -ij_groovy_spaces_around_shift_operators = true -ij_groovy_spaces_within_annotation_parentheses = false -ij_groovy_spaces_within_array_initializer_braces = false -ij_groovy_spaces_within_braces = true -ij_groovy_spaces_within_brackets = false -ij_groovy_spaces_within_cast_parentheses = false -ij_groovy_spaces_within_catch_parentheses = false -ij_groovy_spaces_within_for_parentheses = false -ij_groovy_spaces_within_gstring_injection_braces = false -ij_groovy_spaces_within_if_parentheses = false -ij_groovy_spaces_within_list_or_map = false -ij_groovy_spaces_within_method_call_parentheses = false -ij_groovy_spaces_within_method_parentheses = false -ij_groovy_spaces_within_parentheses = false -ij_groovy_spaces_within_switch_parentheses = false -ij_groovy_spaces_within_synchronized_parentheses = false -ij_groovy_spaces_within_try_parentheses = false -ij_groovy_spaces_within_tuple_expression = false -ij_groovy_spaces_within_while_parentheses = false -ij_groovy_special_else_if_treatment = true -ij_groovy_ternary_operation_wrap = normal -ij_groovy_throws_keyword_wrap = normal -ij_groovy_throws_list_wrap = normal -ij_groovy_use_flying_geese_braces = false -ij_groovy_use_fq_class_names = false -ij_groovy_use_fq_class_names_in_javadoc = false -ij_groovy_use_relative_indents = false -ij_groovy_use_single_class_imports = true -ij_groovy_variable_annotation_wrap = normal -ij_groovy_while_brace_force = always -ij_groovy_while_on_new_line = false -ij_groovy_wrap_chain_calls_after_dot = false -ij_groovy_wrap_long_lines = false - -[{*.har,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,bowerrc,jest.config}] -indent_size = 2 -ij_json_array_wrapping = split_into_lines -ij_json_keep_blank_lines_in_code = 0 -ij_json_keep_indents_on_empty_lines = false -ij_json_keep_line_breaks = false -ij_json_keep_trailing_comma = false -ij_json_object_wrapping = split_into_lines -ij_json_property_alignment = do_not_align -ij_json_space_after_colon = true -ij_json_space_after_comma = true -ij_json_space_before_colon = false -ij_json_space_before_comma = false -ij_json_spaces_within_braces = false -ij_json_spaces_within_brackets = false -ij_json_wrap_long_lines = false - -[{*.htm,*.html,*.ng,*.peb,*.sht,*.shtm,*.shtml}] -ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3 -ij_html_align_attributes = false -ij_html_align_text = false -ij_html_attribute_wrap = normal -ij_html_block_comment_add_space = false -ij_html_block_comment_at_first_column = true -ij_html_do_not_align_children_of_min_lines = 0 -ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p -ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot -ij_html_enforce_quotes = true -ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var -ij_html_keep_blank_lines = 1 -ij_html_keep_indents_on_empty_lines = false -ij_html_keep_line_breaks = true -ij_html_keep_line_breaks_in_text = true -ij_html_keep_whitespaces = false -ij_html_keep_whitespaces_inside = span,pre,textarea -ij_html_line_comment_at_first_column = true -ij_html_new_line_after_last_attribute = never -ij_html_new_line_before_first_attribute = never -ij_html_quote_style = double -ij_html_remove_new_line_before_tags = br -ij_html_space_after_tag_name = false -ij_html_space_around_equality_in_attribute = false -ij_html_space_inside_empty_tag = false -ij_html_text_wrap = normal - -[{*.http,*.rest}] -indent_size = 0 -ij_continuation_indent_size = 4 -ij_http-request_call_parameters_wrap = normal -ij_http-request_method_parameters_wrap = split_into_lines -ij_http-request_space_before_comma = true -ij_http-request_spaces_around_assignment_operators = true - -[{*.jsf,*.jsp,*.jspf,*.tag,*.tagf,*.xjsp}] -ij_jsp_jsp_prefer_comma_separated_import_list = false -ij_jsp_keep_indents_on_empty_lines = false - -[{*.jspx,*.tagx}] -ij_jspx_keep_indents_on_empty_lines = false - -[{*.kt,*.kts}] -ij_kotlin_align_in_columns_case_branch = false -ij_kotlin_align_multiline_binary_operation = false -ij_kotlin_align_multiline_extends_list = false -ij_kotlin_align_multiline_method_parentheses = false -ij_kotlin_align_multiline_parameters = false -ij_kotlin_align_multiline_parameters_in_calls = false -ij_kotlin_allow_trailing_comma = false -ij_kotlin_allow_trailing_comma_on_call_site = false -ij_kotlin_assignment_wrap = normal -ij_kotlin_blank_lines_after_class_header = 0 -ij_kotlin_blank_lines_around_block_when_branches = 0 -ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 -ij_kotlin_block_comment_add_space = false -ij_kotlin_block_comment_at_first_column = true -ij_kotlin_call_parameters_new_line_after_left_paren = false -ij_kotlin_call_parameters_right_paren_on_new_line = false -ij_kotlin_call_parameters_wrap = normal -ij_kotlin_catch_on_new_line = false -ij_kotlin_class_annotation_wrap = split_into_lines -ij_kotlin_continuation_indent_for_chained_calls = true -ij_kotlin_continuation_indent_for_expression_bodies = true -ij_kotlin_continuation_indent_in_argument_lists = true -ij_kotlin_continuation_indent_in_elvis = true -ij_kotlin_continuation_indent_in_if_conditions = true -ij_kotlin_continuation_indent_in_parameter_lists = true -ij_kotlin_continuation_indent_in_supertype_lists = true -ij_kotlin_else_on_new_line = false -ij_kotlin_enum_constants_wrap = split_into_lines -ij_kotlin_extends_list_wrap = normal -ij_kotlin_field_annotation_wrap = split_into_lines -ij_kotlin_finally_on_new_line = false -ij_kotlin_if_rparen_on_new_line = false -ij_kotlin_import_nested_classes = false -ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^ -ij_kotlin_insert_whitespaces_in_simple_one_line_method = true -ij_kotlin_keep_blank_lines_before_right_brace = 1 -ij_kotlin_keep_blank_lines_in_code = 1 -ij_kotlin_keep_blank_lines_in_declarations = 1 -ij_kotlin_keep_first_column_comment = true -ij_kotlin_keep_indents_on_empty_lines = false -ij_kotlin_keep_line_breaks = true -ij_kotlin_lbrace_on_next_line = false -ij_kotlin_line_break_after_multiline_when_entry = true -ij_kotlin_line_comment_add_space = false -ij_kotlin_line_comment_add_space_on_reformat = false -ij_kotlin_line_comment_at_first_column = true -ij_kotlin_method_annotation_wrap = split_into_lines -ij_kotlin_method_call_chain_wrap = normal -ij_kotlin_method_parameters_new_line_after_left_paren = false -ij_kotlin_method_parameters_right_paren_on_new_line = false -ij_kotlin_method_parameters_wrap = normal -ij_kotlin_name_count_to_use_star_import = 2147483647 -ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 -ij_kotlin_packages_to_use_import_on_demand = -ij_kotlin_parameter_annotation_wrap = normal -ij_kotlin_space_after_comma = true -ij_kotlin_space_after_extend_colon = true -ij_kotlin_space_after_type_colon = true -ij_kotlin_space_before_catch_parentheses = true -ij_kotlin_space_before_comma = false -ij_kotlin_space_before_extend_colon = true -ij_kotlin_space_before_for_parentheses = true -ij_kotlin_space_before_if_parentheses = true -ij_kotlin_space_before_lambda_arrow = true -ij_kotlin_space_before_type_colon = false -ij_kotlin_space_before_when_parentheses = true -ij_kotlin_space_before_while_parentheses = true -ij_kotlin_spaces_around_additive_operators = true -ij_kotlin_spaces_around_assignment_operators = true -ij_kotlin_spaces_around_equality_operators = true -ij_kotlin_spaces_around_function_type_arrow = true -ij_kotlin_spaces_around_logical_operators = true -ij_kotlin_spaces_around_multiplicative_operators = true -ij_kotlin_spaces_around_range = false -ij_kotlin_spaces_around_relational_operators = true -ij_kotlin_spaces_around_unary_operator = false -ij_kotlin_spaces_around_when_arrow = true -ij_kotlin_variable_annotation_wrap = normal -ij_kotlin_while_on_new_line = false -ij_kotlin_wrap_elvis_expressions = 1 -ij_kotlin_wrap_expression_body_functions = 1 -ij_kotlin_wrap_first_method_in_call_chain = false - -[{*.markdown,*.md}] -ij_markdown_force_one_space_after_blockquote_symbol = true -ij_markdown_force_one_space_after_header_symbol = true -ij_markdown_force_one_space_after_list_bullet = true -ij_markdown_force_one_space_between_words = true -ij_markdown_format_tables = true -ij_markdown_insert_quote_arrows_on_wrap = true -ij_markdown_keep_indents_on_empty_lines = false -ij_markdown_keep_line_breaks_inside_text_blocks = true -ij_markdown_max_lines_around_block_elements = 1 -ij_markdown_max_lines_around_header = 1 -ij_markdown_max_lines_between_paragraphs = 1 -ij_markdown_min_lines_around_block_elements = 1 -ij_markdown_min_lines_around_header = 1 -ij_markdown_min_lines_between_paragraphs = 1 -ij_markdown_wrap_text_if_long = true -ij_markdown_wrap_text_inside_blockquotes = true - -[{*.pb,*.textproto}] -indent_size = 2 -tab_width = 2 -ij_continuation_indent_size = 4 -ij_prototext_keep_blank_lines_in_code = 2 -ij_prototext_keep_indents_on_empty_lines = false -ij_prototext_keep_line_breaks = true -ij_prototext_space_after_colon = true -ij_prototext_space_after_comma = true -ij_prototext_space_before_colon = false -ij_prototext_space_before_comma = false -ij_prototext_spaces_within_braces = true -ij_prototext_spaces_within_brackets = false - -[{*.properties,spring.handlers,spring.schemas}] -ij_properties_align_group_field_declarations = false -ij_properties_keep_blank_lines = true -ij_properties_key_value_delimiter = colon -ij_properties_spaces_around_key_value_delimiter = false - -[{*.qute.htm,*.qute.html,*.qute.json,*.qute.txt,*.qute.yaml,*.qute.yml}] -ij_qute_keep_indents_on_empty_lines = false - -[{*.toml,Cargo.lock,Cargo.toml.orig,Gopkg.lock,Pipfile,poetry.lock}] -ij_toml_keep_indents_on_empty_lines = false - -[{*.yaml,*.yml}] -indent_size = 2 -ij_yaml_align_values_properties = do_not_align -ij_yaml_autoinsert_sequence_marker = true -ij_yaml_block_mapping_on_new_line = false -ij_yaml_indent_sequence_value = true -ij_yaml_keep_indents_on_empty_lines = false -ij_yaml_keep_line_breaks = true -ij_yaml_sequence_on_new_line = false -ij_yaml_space_before_colon = false -ij_yaml_spaces_within_braces = true -ij_yaml_spaces_within_brackets = true From 3190abb18dd088409236cdc9b0574a4d409d2799 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:37:50 -0600 Subject: [PATCH 81/84] Update Gradle wrapper --- gradle.properties | 3 + gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 43764 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 286 ++++++++++++++--------- gradlew.bat | 41 ++-- 5 files changed, 203 insertions(+), 129 deletions(-) create mode 100644 gradle.properties diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..31634b4e --- /dev/null +++ b/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.logging.level=info +org.gradle.logging.stacktrace=all +org.gradle.jvmargs=-XX:MaxRAMPercentage=100.0 -XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=15 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..1b33c55baabb587c669f562ae36f953de2481846 100644 GIT binary patch literal 43764 zcma&OWmKeVvL#I6?i3D%6z=Zs?ofE*?rw#G$eqJB ziT4y8-Y@s9rkH0Tz>ll(^xkcTl)CY?rS&9VNd66Yc)g^6)JcWaY(5$5gt z8gr3SBXUTN;~cBgz&})qX%#!Fxom2Yau_`&8)+6aSN7YY+pS410rRUU*>J}qL0TnJ zRxt*7QeUqTh8j)Q&iavh<}L+$Jqz))<`IfKussVk%%Ah-Ti?Eo0hQH!rK%K=#EAw0 zwq@@~XNUXRnv8$;zv<6rCRJ6fPD^hfrh;0K?n z=p!u^3xOgWZ%f3+?+>H)9+w^$Tn1e;?UpVMJb!!;f)`6f&4|8mr+g)^@x>_rvnL0< zvD0Hu_N>$(Li7|Jgu0mRh&MV+<}`~Wi*+avM01E)Jtg=)-vViQKax!GeDc!xv$^mL z{#OVBA$U{(Zr8~Xm|cP@odkHC*1R8z6hcLY#N@3E-A8XEvpt066+3t9L_6Zg6j@9Q zj$$%~yO-OS6PUVrM2s)(T4#6=JpI_@Uz+!6=GdyVU?`!F=d;8#ZB@(5g7$A0(`eqY z8_i@3w$0*es5mrSjhW*qzrl!_LQWs4?VfLmo1Sd@Ztt53+etwzAT^8ow_*7Jp`Y|l z*UgSEwvxq+FYO!O*aLf-PinZYne7Ib6ny3u>MjQz=((r3NTEeU4=-i0LBq3H-VJH< z^>1RE3_JwrclUn9vb7HcGUaFRA0QHcnE;6)hnkp%lY1UII#WPAv?-;c?YH}LWB8Nl z{sx-@Z;QxWh9fX8SxLZk8;kMFlGD3Jc^QZVL4nO)1I$zQwvwM&_!kW+LMf&lApv#< zur|EyC|U@5OQuph$TC_ZU`{!vJp`13e9alaR0Dbn5ikLFH7>eIz4QbV|C=%7)F=qo z_>M&5N)d)7G(A%c>}UCrW!Ql_6_A{?R7&CL`;!KOb3 z8Z=$YkV-IF;c7zs{3-WDEFJzuakFbd*4LWd<_kBE8~BFcv}js_2OowRNzWCtCQ6&k z{&~Me92$m*@e0ANcWKuz)?YjB*VoSTx??-3Cc0l2U!X^;Bv@m87eKHukAljrD54R+ zE;@_w4NPe1>3`i5Qy*3^E9x#VB6?}v=~qIprrrd5|DFkg;v5ixo0IsBmik8=Y;zv2 z%Bcf%NE$a44bk^`i4VwDLTbX=q@j9;JWT9JncQ!+Y%2&HHk@1~*L8-{ZpY?(-a9J-1~<1ltr9i~D9`P{XTIFWA6IG8c4;6bFw*lzU-{+?b&%OcIoCiw00n>A1ra zFPE$y@>ebbZlf(sN_iWBzQKDV zmmaLX#zK!@ZdvCANfwV}9@2O&w)!5gSgQzHdk2Q`jG6KD7S+1R5&F)j6QTD^=hq&7 zHUW+r^da^%V(h(wonR(j?BOiC!;y=%nJvz?*aW&5E87qq;2z`EI(f zBJNNSMFF9U{sR-af5{IY&AtoGcoG)Iq-S^v{7+t0>7N(KRoPj;+2N5;9o_nxIGjJ@ z7bYQK)bX)vEhy~VL%N6g^NE@D5VtV+Q8U2%{ji_=6+i^G%xeskEhH>Sqr194PJ$fB zu1y^){?9Vkg(FY2h)3ZHrw0Z<@;(gd_dtF#6y_;Iwi{yX$?asr?0N0_B*CifEi7<6 zq`?OdQjCYbhVcg+7MSgIM|pJRu~`g?g3x?Tl+V}#$It`iD1j+!x+!;wS0+2e>#g?Z z*EA^k7W{jO1r^K~cD#5pamp+o@8&yw6;%b|uiT?{Wa=4+9<}aXWUuL#ZwN1a;lQod zW{pxWCYGXdEq9qAmvAB904}?97=re$>!I%wxPV#|f#@A*Y=qa%zHlDv^yWbR03%V0 zprLP+b(#fBqxI%FiF*-n8HtH6$8f(P6!H3V^ysgd8de-N(@|K!A< z^qP}jp(RaM9kQ(^K(U8O84?D)aU(g?1S8iWwe)gqpHCaFlJxb*ilr{KTnu4_@5{K- z)n=CCeCrPHO0WHz)dDtkbZfUfVBd?53}K>C5*-wC4hpDN8cGk3lu-ypq+EYpb_2H; z%vP4@&+c2p;thaTs$dc^1CDGlPG@A;yGR5@$UEqk6p58qpw#7lc<+W(WR;(vr(D>W z#(K$vE#uBkT=*q&uaZwzz=P5mjiee6>!lV?c}QIX%ZdkO1dHg>Fa#xcGT6~}1*2m9 zkc7l3ItD6Ie~o_aFjI$Ri=C!8uF4!Ky7iG9QTrxVbsQroi|r)SAon#*B*{}TB-?=@ z8~jJs;_R2iDd!$+n$%X6FO&PYS{YhDAS+U2o4su9x~1+U3z7YN5o0qUK&|g^klZ6X zj_vrM5SUTnz5`*}Hyts9ADwLu#x_L=nv$Z0`HqN`Zo=V>OQI)fh01n~*a%01%cx%0 z4LTFVjmW+ipVQv5rYcn3;d2o4qunWUY!p+?s~X~(ost@WR@r@EuDOSs8*MT4fiP>! zkfo^!PWJJ1MHgKS2D_hc?Bs?isSDO61>ebl$U*9*QY(b=i&rp3@3GV@z>KzcZOxip z^dzA~44;R~cnhWz7s$$v?_8y-k!DZys}Q?4IkSyR!)C0j$(Gm|t#e3|QAOFaV2}36 z?dPNY;@I=FaCwylc_;~kXlZsk$_eLkNb~TIl8QQ`mmH&$*zwwR8zHU*sId)rxHu*K z;yZWa8UmCwju%aSNLwD5fBl^b0Ux1%q8YR*uG`53Mi<`5uA^Dc6Ync)J3N7;zQ*75)hf%a@{$H+%S?SGT)ks60)?6j$ zspl|4Ad6@%-r1t*$tT(en!gIXTUDcsj?28ZEzz)dH)SV3bZ+pjMaW0oc~rOPZP@g! zb9E+ndeVO_Ib9c_>{)`01^`ZS198 z)(t=+{Azi11$eu%aU7jbwuQrO`vLOixuh~%4z@mKr_Oc;F%Uq01fA)^W&y+g16e?rkLhTxV!EqC%2}sx_1u7IBq|}Be&7WI z4I<;1-9tJsI&pQIhj>FPkQV9{(m!wYYV@i5h?A0#BN2wqlEwNDIq06|^2oYVa7<~h zI_OLan0Do*4R5P=a3H9`s5*>xU}_PSztg`+2mv)|3nIy=5#Z$%+@tZnr> zLcTI!Mxa`PY7%{;KW~!=;*t)R_sl<^b>eNO@w#fEt(tPMg_jpJpW$q_DoUlkY|uo> z0-1{ouA#;t%spf*7VjkK&$QrvwUERKt^Sdo)5@?qAP)>}Y!h4(JQ!7{wIdkA+|)bv z&8hBwoX4v|+fie}iTslaBX^i*TjwO}f{V)8*!dMmRPi%XAWc8<_IqK1jUsApk)+~R zNFTCD-h>M5Y{qTQ&0#j@I@tmXGj%rzhTW5%Bkh&sSc=$Fv;M@1y!zvYG5P2(2|(&W zlcbR1{--rJ&s!rB{G-sX5^PaM@3EqWVz_y9cwLR9xMig&9gq(voeI)W&{d6j1jh&< zARXi&APWE1FQWh7eoZjuP z;vdgX>zep^{{2%hem;e*gDJhK1Hj12nBLIJoL<=0+8SVEBx7!4Ea+hBY;A1gBwvY<)tj~T=H`^?3>zeWWm|LAwo*S4Z%bDVUe z6r)CH1H!(>OH#MXFJ2V(U(qxD{4Px2`8qfFLG+=a;B^~Te_Z!r3RO%Oc#ZAHKQxV5 zRYXxZ9T2A%NVJIu5Pu7!Mj>t%YDO$T@M=RR(~mi%sv(YXVl`yMLD;+WZ{vG9(@P#e zMo}ZiK^7^h6TV%cG+;jhJ0s>h&VERs=tuZz^Tlu~%d{ZHtq6hX$V9h)Bw|jVCMudd zwZ5l7In8NT)qEPGF$VSKg&fb0%R2RnUnqa){)V(X(s0U zkCdVZe6wy{+_WhZh3qLp245Y2RR$@g-!9PjJ&4~0cFSHMUn=>dapv)hy}|y91ZWTV zCh=z*!S3_?`$&-eZ6xIXUq8RGl9oK0BJw*TdU6A`LJqX9eS3X@F)g$jLkBWFscPhR zpCv8#KeAc^y>>Y$k^=r|K(DTC}T$0#jQBOwB#@`P6~*IuW_8JxCG}J4va{ zsZzt}tt+cv7=l&CEuVtjD6G2~_Meh%p4RGuY?hSt?(sreO_F}8r7Kp$qQdvCdZnDQ zxzc*qchE*E2=WK)^oRNa>Ttj`fpvF-JZ5tu5>X1xw)J@1!IqWjq)ESBG?J|ez`-Tc zi5a}GZx|w-h%5lNDE_3ho0hEXMoaofo#Z;$8|2;EDF&*L+e$u}K=u?pb;dv$SXeQM zD-~7P0i_`Wk$#YP$=hw3UVU+=^@Kuy$>6?~gIXx636jh{PHly_a2xNYe1l60`|y!7 z(u%;ILuW0DDJ)2%y`Zc~hOALnj1~txJtcdD#o4BCT68+8gZe`=^te6H_egxY#nZH&P*)hgYaoJ^qtmpeea`35Fw)cy!w@c#v6E29co8&D9CTCl%^GV|X;SpneSXzV~LXyRn-@K0Df z{tK-nDWA!q38M1~`xUIt_(MO^R(yNY#9@es9RQbY@Ia*xHhD&=k^T+ zJi@j2I|WcgW=PuAc>hs`(&CvgjL2a9Rx zCbZyUpi8NWUOi@S%t+Su4|r&UoU|ze9SVe7p@f1GBkrjkkq)T}X%Qo1g!SQ{O{P?m z-OfGyyWta+UCXH+-+(D^%kw#A1-U;?9129at7MeCCzC{DNgO zeSqsV>W^NIfTO~4({c}KUiuoH8A*J!Cb0*sp*w-Bg@YfBIPZFH!M}C=S=S7PLLcIG zs7K77g~W)~^|+mx9onzMm0qh(f~OsDTzVmRtz=aZTllgR zGUn~_5hw_k&rll<4G=G+`^Xlnw;jNYDJz@bE?|r866F2hA9v0-8=JO3g}IHB#b`hy zA42a0>{0L7CcabSD+F7?pGbS1KMvT{@1_@k!_+Ki|5~EMGt7T%u=79F)8xEiL5!EJ zzuxQ`NBliCoJMJdwu|);zRCD<5Sf?Y>U$trQ-;xj6!s5&w=9E7)%pZ+1Nh&8nCCwM zv5>Ket%I?cxr3vVva`YeR?dGxbG@pi{H#8@kFEf0Jq6~K4>kt26*bxv=P&jyE#e$| zDJB_~imk^-z|o!2njF2hL*|7sHCnzluhJjwLQGDmC)Y9 zr9ZN`s)uCd^XDvn)VirMgW~qfn1~SaN^7vcX#K1G`==UGaDVVx$0BQnubhX|{e z^i0}>k-;BP#Szk{cFjO{2x~LjK{^Upqd&<+03_iMLp0$!6_$@TbX>8U-f*-w-ew1?`CtD_0y_Lo|PfKi52p?`5$Jzx0E8`M0 zNIb?#!K$mM4X%`Ry_yhG5k@*+n4||2!~*+&pYLh~{`~o(W|o64^NrjP?-1Lgu?iK^ zTX6u3?#$?R?N!{599vg>G8RGHw)Hx&=|g4599y}mXNpM{EPKKXB&+m?==R3GsIq?G zL5fH={=zawB(sMlDBJ+{dgb)Vx3pu>L=mDV0{r1Qs{0Pn%TpopH{m(By4;{FBvi{I z$}x!Iw~MJOL~&)p93SDIfP3x%ROjg}X{Sme#hiJ&Yk&a;iR}V|n%PriZBY8SX2*;6 z4hdb^&h;Xz%)BDACY5AUsV!($lib4>11UmcgXKWpzRL8r2Srl*9Y(1uBQsY&hO&uv znDNff0tpHlLISam?o(lOp#CmFdH<6HmA0{UwfU#Y{8M+7od8b8|B|7ZYR9f<#+V|ZSaCQvI$~es~g(Pv{2&m_rKSB2QQ zMvT}$?Ll>V+!9Xh5^iy3?UG;dF-zh~RL#++roOCsW^cZ&({6q|?Jt6`?S8=16Y{oH zp50I7r1AC1(#{b`Aq5cw>ypNggHKM9vBx!W$eYIzD!4KbLsZGr2o8>g<@inmS3*>J zx8oG((8f!ei|M@JZB`p7+n<Q}?>h249<`7xJ?u}_n;Gq(&km#1ULN87CeTO~FY zS_Ty}0TgQhV zOh3T7{{x&LSYGQfKR1PDIkP!WnfC1$l+fs@Di+d4O=eVKeF~2fq#1<8hEvpwuqcaH z4A8u~r^gnY3u6}zj*RHjk{AHhrrDqaj?|6GaVJbV%o-nATw}ASFr!f`Oz|u_QPkR# z0mDudY1dZRlk@TyQ?%Eti=$_WNFtLpSx9=S^be{wXINp%MU?a`F66LNU<c;0&ngifmP9i;bj6&hdGMW^Kf8e6ZDXbQD&$QAAMo;OQ)G zW(qlHh;}!ZP)JKEjm$VZjTs@hk&4{?@+NADuYrr!R^cJzU{kGc1yB?;7mIyAWwhbeA_l_lw-iDVi7wcFurf5 z#Uw)A@a9fOf{D}AWE%<`s1L_AwpZ?F!Vac$LYkp<#A!!`XKaDC{A%)~K#5z6>Hv@V zBEqF(D5?@6r3Pwj$^krpPDCjB+UOszqUS;b2n>&iAFcw<*im2(b3|5u6SK!n9Sg4I z0KLcwA6{Mq?p%t>aW0W!PQ>iUeYvNjdKYqII!CE7SsS&Rj)eIw-K4jtI?II+0IdGq z2WT|L3RL?;GtGgt1LWfI4Ka`9dbZXc$TMJ~8#Juv@K^1RJN@yzdLS8$AJ(>g!U9`# zx}qr7JWlU+&m)VG*Se;rGisutS%!6yybi%B`bv|9rjS(xOUIvbNz5qtvC$_JYY+c& za*3*2$RUH8p%pSq>48xR)4qsp!Q7BEiJ*`^>^6INRbC@>+2q9?x(h0bpc>GaNFi$K zPH$6!#(~{8@0QZk=)QnM#I=bDx5vTvjm$f4K}%*s+((H2>tUTf==$wqyoI`oxI7>C z&>5fe)Yg)SmT)eA(|j@JYR1M%KixxC-Eceknf-;N=jJTwKvk#@|J^&5H0c+%KxHUI z6dQbwwVx3p?X<_VRVb2fStH?HH zFR@Mp=qX%#L3XL)+$PXKV|o|#DpHAoqvj6uQKe@M-mnhCSou7Dj4YuO6^*V`m)1lf z;)@e%1!Qg$10w8uEmz{ENb$^%u}B;J7sDd zump}onoD#!l=agcBR)iG!3AF0-63%@`K9G(CzKrm$VJ{v7^O9Ps7Zej|3m= zVXlR&yW6=Y%mD30G@|tf=yC7-#L!16Q=dq&@beWgaIL40k0n% z)QHrp2Jck#evLMM1RGt3WvQ936ZC9vEje0nFMfvmOHVI+&okB_K|l-;|4vW;qk>n~ z+|kk8#`K?x`q>`(f6A${wfw9Cx(^)~tX7<#TpxR#zYG2P+FY~mG{tnEkv~d6oUQA+ z&hNTL=~Y@rF`v-RZlts$nb$3(OL1&@Y11hhL9+zUb6)SP!;CD)^GUtUpCHBE`j1te zAGud@miCVFLk$fjsrcpjsadP__yj9iEZUW{Ll7PPi<$R;m1o!&Xdl~R_v0;oDX2z^!&8}zNGA}iYG|k zmehMd1%?R)u6R#<)B)1oe9TgYH5-CqUT8N7K-A-dm3hbm_W21p%8)H{O)xUlBVb+iUR}-v5dFaCyfSd zC6Bd7=N4A@+Bna=!-l|*_(nWGDpoyU>nH=}IOrLfS+-d40&(Wo*dDB9nQiA2Tse$R z;uq{`X7LLzP)%Y9aHa4YQ%H?htkWd3Owv&UYbr5NUDAH^<l@Z0Cx%`N+B*i!!1u>D8%;Qt1$ zE5O0{-`9gdDxZ!`0m}ywH!;c{oBfL-(BH<&SQ~smbcobU!j49O^f4&IIYh~f+hK*M zZwTp%{ZSAhMFj1qFaOA+3)p^gnXH^=)`NTYgTu!CLpEV2NF=~-`(}7p^Eof=@VUbd z_9U|8qF7Rueg&$qpSSkN%%%DpbV?8E8ivu@ensI0toJ7Eas^jyFReQ1JeY9plb^{m z&eQO)qPLZQ6O;FTr*aJq=$cMN)QlQO@G&%z?BKUs1&I^`lq>=QLODwa`(mFGC`0H< zOlc*|N?B5&!U6BuJvkL?s1&nsi$*5cCv7^j_*l&$-sBmRS85UIrE--7eD8Gr3^+o? zqG-Yl4S&E;>H>k^a0GdUI(|n1`ws@)1%sq2XBdK`mqrNq_b4N{#VpouCXLzNvjoFv zo9wMQ6l0+FT+?%N(ka*;%m~(?338bu32v26!{r)|w8J`EL|t$}TA4q_FJRX5 zCPa{hc_I(7TGE#@rO-(!$1H3N-C0{R$J=yPCXCtGk{4>=*B56JdXU9cQVwB`6~cQZ zf^qK21x_d>X%dT!!)CJQ3mlHA@ z{Prkgfs6=Tz%63$6Zr8CO0Ak3A)Cv#@BVKr&aiKG7RYxY$Yx>Bj#3gJk*~Ps-jc1l z;4nltQwwT4@Z)}Pb!3xM?+EW0qEKA)sqzw~!C6wd^{03-9aGf3Jmt=}w-*!yXupLf z;)>-7uvWN4Unn8b4kfIza-X=x*e4n5pU`HtgpFFd))s$C@#d>aUl3helLom+RYb&g zI7A9GXLRZPl}iQS*d$Azxg-VgcUr*lpLnbPKUV{QI|bsG{8bLG<%CF( zMoS4pRDtLVYOWG^@ox^h8xL~afW_9DcE#^1eEC1SVSb1BfDi^@g?#f6e%v~Aw>@w- zIY0k+2lGWNV|aA*e#`U3=+oBDmGeInfcL)>*!w|*;mWiKNG6wP6AW4-4imN!W)!hE zA02~S1*@Q`fD*+qX@f3!2yJX&6FsEfPditB%TWo3=HA;T3o2IrjS@9SSxv%{{7&4_ zdS#r4OU41~GYMiib#z#O;zohNbhJknrPPZS6sN$%HB=jUnlCO_w5Gw5EeE@KV>soy z2EZ?Y|4RQDDjt5y!WBlZ(8M)|HP<0YyG|D%RqD+K#e7-##o3IZxS^wQ5{Kbzb6h(i z#(wZ|^ei>8`%ta*!2tJzwMv+IFHLF`zTU8E^Mu!R*45_=ccqI};Zbyxw@U%a#2}%f zF>q?SrUa_a4H9l+uW8JHh2Oob>NyUwG=QH~-^ZebU*R@67DcXdz2{HVB4#@edz?B< z5!rQH3O0>A&ylROO%G^fimV*LX7>!%re{_Sm6N>S{+GW1LCnGImHRoF@csnFzn@P0 zM=jld0z%oz;j=>c7mMwzq$B^2mae7NiG}%>(wtmsDXkWk{?BeMpTrIt3Mizq?vRsf zi_WjNp+61uV(%gEU-Vf0;>~vcDhe(dzWdaf#4mH3o^v{0EWhj?E?$5v02sV@xL0l4 zX0_IMFtQ44PfWBbPYN#}qxa%=J%dlR{O!KyZvk^g5s?sTNycWYPJ^FK(nl3k?z-5t z39#hKrdO7V(@!TU)LAPY&ngnZ1MzLEeEiZznn7e-jLCy8LO zu^7_#z*%I-BjS#Pg-;zKWWqX-+Ly$T!4`vTe5ZOV0j?TJVA*2?*=82^GVlZIuH%9s zXiV&(T(QGHHah=s&7e|6y?g+XxZGmK55`wGV>@1U)Th&=JTgJq>4mI&Av2C z)w+kRoj_dA!;SfTfkgMPO>7Dw6&1*Hi1q?54Yng`JO&q->^CX21^PrU^JU#CJ_qhV zSG>afB%>2fx<~g8p=P8Yzxqc}s@>>{g7}F!;lCXvF#RV)^fyYb_)iKVCz1xEq=fJ| z0a7DMCK*FuP=NM*5h;*D`R4y$6cpW-E&-i{v`x=Jbk_xSn@2T3q!3HoAOB`@5Vg6) z{PW|@9o!e;v1jZ2{=Uw6S6o{g82x6g=k!)cFSC*oemHaVjg?VpEmtUuD2_J^A~$4* z3O7HsbA6wxw{TP5Kk)(Vm?gKo+_}11vbo{Tp_5x79P~#F)ahQXT)tSH5;;14?s)On zel1J>1x>+7;g1Iz2FRpnYz;sD0wG9Q!vuzE9yKi3@4a9Nh1!GGN?hA)!mZEnnHh&i zf?#ZEN2sFbf~kV;>K3UNj1&vFhc^sxgj8FCL4v>EOYL?2uuT`0eDH}R zmtUJMxVrV5H{L53hu3#qaWLUa#5zY?f5ozIn|PkMWNP%n zWB5!B0LZB0kLw$k39=!akkE9Q>F4j+q434jB4VmslQ;$ zKiO#FZ`p|dKS716jpcvR{QJkSNfDVhr2%~eHrW;fU45>>snr*S8Vik-5eN5k*c2Mp zyxvX&_cFbB6lODXznHHT|rsURe2!swomtrqc~w5 zymTM8!w`1{04CBprR!_F{5LB+2_SOuZN{b*!J~1ZiPpP-M;);!ce!rOPDLtgR@Ie1 zPreuqm4!H)hYePcW1WZ0Fyaqe%l}F~Orr)~+;mkS&pOhP5Ebb`cnUt!X_QhP4_4p( z8YKQCDKGIy>?WIFm3-}Br2-N`T&FOi?t)$hjphB9wOhBXU#Hb+zm&We_-O)s(wc`2 z8?VsvU;J>Ju7n}uUb3s1yPx_F*|FlAi=Ge=-kN?1;`~6szP%$3B0|8Sqp%ebM)F8v zADFrbeT0cgE>M0DMV@_Ze*GHM>q}wWMzt|GYC%}r{OXRG3Ij&<+nx9;4jE${Fj_r* z`{z1AW_6Myd)i6e0E-h&m{{CvzH=Xg!&(bLYgRMO_YVd8JU7W+7MuGWNE=4@OvP9+ zxi^vqS@5%+#gf*Z@RVyU9N1sO-(rY$24LGsg1>w>s6ST^@)|D9>cT50maXLUD{Fzf zt~tp{OSTEKg3ZSQyQQ5r51){%=?xlZ54*t1;Ow)zLe3i?8tD8YyY^k%M)e`V*r+vL zPqUf&m)U+zxps+NprxMHF{QSxv}>lE{JZETNk1&F+R~bp{_T$dbXL2UGnB|hgh*p4h$clt#6;NO~>zuyY@C-MD@)JCc5XrYOt`wW7! z_ti2hhZBMJNbn0O-uTxl_b6Hm313^fG@e;RrhIUK9@# z+DHGv_Ow$%S8D%RB}`doJjJy*aOa5mGHVHz0e0>>O_%+^56?IkA5eN+L1BVCp4~m=1eeL zb;#G!#^5G%6Mw}r1KnaKsLvJB%HZL)!3OxT{k$Yo-XrJ?|7{s4!H+S2o?N|^Z z)+?IE9H7h~Vxn5hTis^3wHYuOU84+bWd)cUKuHapq=&}WV#OxHpLab`NpwHm8LmOo zjri+!k;7j_?FP##CpM+pOVx*0wExEex z@`#)K<-ZrGyArK;a%Km`^+We|eT+#MygHOT6lXBmz`8|lyZOwL1+b+?Z$0OhMEp3R z&J=iRERpv~TC=p2-BYLC*?4 zxvPs9V@g=JT0>zky5Poj=fW_M!c)Xxz1<=&_ZcL=LMZJqlnO1P^xwGGW*Z+yTBvbV z-IFe6;(k1@$1;tS>{%pXZ_7w+i?N4A2=TXnGf=YhePg8bH8M|Lk-->+w8Y+FjZ;L=wSGwxfA`gqSn)f(XNuSm>6Y z@|#e-)I(PQ^G@N`%|_DZSb4_pkaEF0!-nqY+t#pyA>{9^*I-zw4SYA1_z2Bs$XGUZbGA;VeMo%CezHK0lO={L%G)dI-+8w?r9iexdoB{?l zbJ}C?huIhWXBVs7oo{!$lOTlvCLZ_KN1N+XJGuG$rh<^eUQIqcI7^pmqhBSaOKNRq zrx~w^?9C?*&rNwP_SPYmo;J-#!G|{`$JZK7DxsM3N^8iR4vvn>E4MU&Oe1DKJvLc~ zCT>KLZ1;t@My zRj_2hI^61T&LIz)S!+AQIV23n1>ng+LUvzv;xu!4;wpqb#EZz;F)BLUzT;8UA1x*6vJ zicB!3Mj03s*kGV{g`fpC?V^s(=JG-k1EMHbkdP4P*1^8p_TqO|;!Zr%GuP$8KLxuf z=pv*H;kzd;P|2`JmBt~h6|GxdU~@weK5O=X&5~w$HpfO}@l-T7@vTCxVOwCkoPQv8 z@aV_)I5HQtfs7^X=C03zYmH4m0S!V@JINm6#(JmZRHBD?T!m^DdiZJrhKpBcur2u1 zf9e4%k$$vcFopK5!CC`;ww(CKL~}mlxK_Pv!cOsFgVkNIghA2Au@)t6;Y3*2gK=5d z?|@1a)-(sQ%uFOmJ7v2iG&l&m^u&^6DJM#XzCrF%r>{2XKyxLD2rgWBD;i(!e4InDQBDg==^z;AzT2z~OmV0!?Z z0S9pX$+E;w3WN;v&NYT=+G8hf=6w0E1$0AOr61}eOvE8W1jX%>&Mjo7&!ulawgzLH zbcb+IF(s^3aj12WSi#pzIpijJJzkP?JzRawnxmNDSUR#7!29vHULCE<3Aa#be}ie~d|!V+ z%l~s9Odo$G&fH!t!+`rUT0T9DulF!Yq&BfQWFZV1L9D($r4H(}Gnf6k3^wa7g5|Ws zj7%d`!3(0bb55yhC6@Q{?H|2os{_F%o=;-h{@Yyyn*V7?{s%Grvpe!H^kl6tF4Zf5 z{Jv1~yZ*iIWL_9C*8pBMQArfJJ0d9Df6Kl#wa}7Xa#Ef_5B7=X}DzbQXVPfCwTO@9+@;A^Ti6il_C>g?A-GFwA0#U;t4;wOm-4oS})h z5&on>NAu67O?YCQr%7XIzY%LS4bha9*e*4bU4{lGCUmO2UQ2U)QOqClLo61Kx~3dI zmV3*(P6F_Tr-oP%x!0kTnnT?Ep5j;_IQ^pTRp=e8dmJtI4YgWd0}+b2=ATkOhgpXe z;jmw+FBLE}UIs4!&HflFr4)vMFOJ19W4f2^W(=2)F%TAL)+=F>IE$=e=@j-*bFLSg z)wf|uFQu+!=N-UzSef62u0-C8Zc7 zo6@F)c+nZA{H|+~7i$DCU0pL{0Ye|fKLuV^w!0Y^tT$isu%i1Iw&N|tX3kwFKJN(M zXS`k9js66o$r)x?TWL}Kxl`wUDUpwFx(w4Yk%49;$sgVvT~n8AgfG~HUcDt1TRo^s zdla@6heJB@JV z!vK;BUMznhzGK6PVtj0)GB=zTv6)Q9Yt@l#fv7>wKovLobMV-+(8)NJmyF8R zcB|_K7=FJGGn^X@JdFaat0uhKjp3>k#^&xE_}6NYNG?kgTp>2Iu?ElUjt4~E-?`Du z?mDCS9wbuS%fU?5BU@Ijx>1HG*N?gIP+<~xE4u=>H`8o((cS5M6@_OK%jSjFHirQK zN9@~NXFx*jS{<|bgSpC|SAnA@I)+GB=2W|JJChLI_mx+-J(mSJ!b)uUom6nH0#2^(L@JBlV#t zLl?j54s`Y3vE^c_3^Hl0TGu*tw_n?@HyO@ZrENxA+^!)OvUX28gDSF*xFtQzM$A+O zCG=n#6~r|3zt=8%GuG} z<#VCZ%2?3Q(Ad#Y7GMJ~{U3>E{5e@z6+rgZLX{Cxk^p-7dip^d29;2N1_mm4QkASo z-L`GWWPCq$uCo;X_BmGIpJFBlhl<8~EG{vOD1o|X$aB9KPhWO_cKiU*$HWEgtf=fn zsO%9bp~D2c@?*K9jVN@_vhR03>M_8h!_~%aN!Cnr?s-!;U3SVfmhRwk11A^8Ns`@KeE}+ zN$H}a1U6E;*j5&~Og!xHdfK5M<~xka)x-0N)K_&e7AjMz`toDzasH+^1bZlC!n()crk9kg@$(Y{wdKvbuUd04N^8}t1iOgsKF zGa%%XWx@WoVaNC1!|&{5ZbkopFre-Lu(LCE5HWZBoE#W@er9W<>R=^oYxBvypN#x3 zq#LC8&q)GFP=5^-bpHj?LW=)-g+3_)Ylps!3^YQ{9~O9&K)xgy zMkCWaApU-MI~e^cV{Je75Qr7eF%&_H)BvfyKL=gIA>;OSq(y z052BFz3E(Prg~09>|_Z@!qj}@;8yxnw+#Ej0?Rk<y}4ghbD569B{9hSFr*^ygZ zr6j7P#gtZh6tMk6?4V$*Jgz+#&ug;yOr>=qdI#9U&^am2qoh4Jy}H2%a|#Fs{E(5r z%!ijh;VuGA6)W)cJZx+;9Bp1LMUzN~x_8lQ#D3+sL{be-Jyeo@@dv7XguJ&S5vrH` z>QxOMWn7N-T!D@1(@4>ZlL^y5>m#0!HKovs12GRav4z!>p(1~xok8+_{| z#Ae4{9#NLh#Vj2&JuIn5$d6t@__`o}umFo(n0QxUtd2GKCyE+erwXY?`cm*h&^9*8 zJ+8x6fRZI-e$CRygofIQN^dWysCxgkyr{(_oBwwSRxZora1(%(aC!5BTtj^+YuevI zx?)H#(xlALUp6QJ!=l9N__$cxBZ5p&7;qD3PsXRFVd<({Kh+mShFWJNpy`N@ab7?9 zv5=klvCJ4bx|-pvOO2-+G)6O?$&)ncA#Urze2rlBfp#htudhx-NeRnJ@u%^_bfw4o z4|{b8SkPV3b>Wera1W(+N@p9H>dc6{cnkh-sgr?e%(YkWvK+0YXVwk0=d`)}*47*B z5JGkEdVix!w7-<%r0JF~`ZMMPe;f0EQHuYHxya`puazyph*ZSb1mJAt^k4549BfS; zK7~T&lRb=W{s&t`DJ$B}s-eH1&&-wEOH1KWsKn0a(ZI+G!v&W4A*cl>qAvUv6pbUR z#(f#EKV8~hk&8oayBz4vaswc(?qw1vn`yC zZQDl2PCB-&Uu@g9ZQHhO+v(W0bNig{-k0;;`+wM@#@J)8r?qOYs#&vUna8ILxN7S{ zp1s41KnR8miQJtJtOr|+qk}wrLt+N*z#5o`TmD1)E&QD(Vh&pjZJ_J*0!8dy_ z>^=@v=J)C`x&gjqAYu`}t^S=DFCtc0MkBU2zf|69?xW`Ck~(6zLD)gSE{7n~6w8j_ zoH&~$ED2k5-yRa0!r8fMRy z;QjBYUaUnpd}mf%iVFPR%Dg9!d>g`01m~>2s))`W|5!kc+_&Y>wD@@C9%>-lE`WB0 zOIf%FVD^cj#2hCkFgi-fgzIfOi+ya)MZK@IZhHT5FVEaSbv-oDDs0W)pA0&^nM0TW zmgJmd7b1R7b0a`UwWJYZXp4AJPteYLH>@M|xZFKwm!t3D3&q~av?i)WvAKHE{RqpD{{%OhYkK?47}+}` zrR2(Iv9bhVa;cDzJ%6ntcSbx7v7J@Y4x&+eWSKZ*eR7_=CVIUSB$^lfYe@g+p|LD{ zPSpQmxx@b$%d!05|H}WzBT4_cq?@~dvy<7s&QWtieJ9)hd4)$SZz}#H2UTi$CkFWW|I)v_-NjuH!VypONC=1`A=rm_jfzQ8Fu~1r8i{q-+S_j$ z#u^t&Xnfi5tZtl@^!fUJhx@~Cg0*vXMK}D{>|$#T*+mj(J_@c{jXBF|rm4-8%Z2o! z2z0o(4%8KljCm^>6HDK!{jI7p+RAPcty_~GZ~R_+=+UzZ0qzOwD=;YeZt*?3%UGdr z`c|BPE;yUbnyARUl&XWSNJ<+uRt%!xPF&K;(l$^JcA_CMH6)FZt{>6ah$|(9$2fc~ z=CD00uHM{qv;{Zk9FR0~u|3|Eiqv9?z2#^GqylT5>6JNZwKqKBzzQpKU2_pmtD;CT zi%Ktau!Y2Tldfu&b0UgmF(SSBID)15*r08eoUe#bT_K-G4VecJL2Pa=6D1K6({zj6 za(2Z{r!FY5W^y{qZ}08+h9f>EKd&PN90f}Sc0ejf%kB4+f#T8Q1=Pj=~#pi$U zp#5rMR%W25>k?<$;$x72pkLibu1N|jX4cWjD3q^Pk3js!uK6h7!dlvw24crL|MZs_ zb%Y%?Fyp0bY0HkG^XyS76Ts*|Giw{31LR~+WU5NejqfPr73Rp!xQ1mLgq@mdWncLy z%8}|nzS4P&`^;zAR-&nm5f;D-%yNQPwq4N7&yULM8bkttkD)hVU>h>t47`{8?n2&4 zjEfL}UEagLUYwdx0sB2QXGeRmL?sZ%J!XM`$@ODc2!y|2#7hys=b$LrGbvvjx`Iqi z&RDDm3YBrlKhl`O@%%&rhLWZ*ABFz2nHu7k~3@e4)kO3%$=?GEFUcCF=6-1n!x^vmu+Ai*amgXH+Rknl6U>#9w;A} zn2xanZSDu`4%%x}+~FG{Wbi1jo@wqBc5(5Xl~d0KW(^Iu(U3>WB@-(&vn_PJt9{1`e9Iic@+{VPc`vP776L*viP{wYB2Iff8hB%E3|o zGMOu)tJX!`qJ}ZPzq7>=`*9TmETN7xwU;^AmFZ-ckZjV5B2T09pYliaqGFY|X#E-8 z20b>y?(r-Fn5*WZ-GsK}4WM>@TTqsxvSYWL6>18q8Q`~JO1{vLND2wg@58OaU!EvT z1|o+f1mVXz2EKAbL!Q=QWQKDZpV|jznuJ}@-)1&cdo z^&~b4Mx{*1gurlH;Vhk5g_cM&6LOHS2 zRkLfO#HabR1JD4Vc2t828dCUG#DL}f5QDSBg?o)IYYi@_xVwR2w_ntlpAW0NWk$F1 z$If?*lP&Ka1oWfl!)1c3fl`g*lMW3JOn#)R1+tfwrs`aiFUgz3;XIJ>{QFxLCkK30 zNS-)#DON3yb!7LBHQJ$)4y%TN82DC2-9tOIqzhZ27@WY^<6}vXCWcR5iN{LN8{0u9 zNXayqD=G|e?O^*ms*4P?G%o@J1tN9_76e}E#66mr89%W_&w4n66~R;X_vWD(oArwj z4CpY`)_mH2FvDuxgT+akffhX0b_slJJ*?Jn3O3~moqu2Fs1oL*>7m=oVek2bnprnW zixkaIFU%+3XhNA@@9hyhFwqsH2bM|`P?G>i<-gy>NflhrN{$9?LZ1ynSE_Mj0rADF zhOz4FnK}wpLmQuV zgO4_Oz9GBu_NN>cPLA=`SP^$gxAnj;WjJnBi%Q1zg`*^cG;Q)#3Gv@c^j6L{arv>- zAW%8WrSAVY1sj$=umcAf#ZgC8UGZGoamK}hR7j6}i8#np8ruUlvgQ$j+AQglFsQQq zOjyHf22pxh9+h#n$21&$h?2uq0>C9P?P=Juw0|;oE~c$H{#RGfa>| zj)Iv&uOnaf@foiBJ}_;zyPHcZt1U~nOcNB{)og8Btv+;f@PIT*xz$x!G?u0Di$lo7 zOugtQ$Wx|C($fyJTZE1JvR~i7LP{ zbdIwqYghQAJi9p}V&$=*2Azev$6K@pyblphgpv8^9bN!?V}{BkC!o#bl&AP!3DAjM zmWFsvn2fKWCfjcAQmE+=c3Y7j@#7|{;;0f~PIodmq*;W9Fiak|gil6$w3%b_Pr6K_ zJEG@&!J%DgBZJDCMn^7mk`JV0&l07Bt`1ymM|;a)MOWz*bh2#d{i?SDe9IcHs7 zjCrnyQ*Y5GzIt}>`bD91o#~5H?4_nckAgotN{2%!?wsSl|LVmJht$uhGa+HiH>;av z8c?mcMYM7;mvWr6noUR{)gE!=i7cZUY7e;HXa221KkRoc2UB>s$Y(k%NzTSEr>W(u z<(4mcc)4rB_&bPzX*1?*ra%VF}P1nwiP5cykJ&W{!OTlz&Td0pOkVp+wc z@k=-Hg=()hNg=Q!Ub%`BONH{ z_=ZFgetj@)NvppAK2>8r!KAgi>#%*7;O-o9MOOfQjV-n@BX6;Xw;I`%HBkk20v`qoVd0)}L6_49y1IhR z_OS}+eto}OPVRn*?UHC{eGyFU7JkPz!+gX4P>?h3QOwGS63fv4D1*no^6PveUeE5% zlehjv_3_^j^C({a2&RSoVlOn71D8WwMu9@Nb@=E_>1R*ve3`#TF(NA0?d9IR_tm=P zOP-x;gS*vtyE1Cm zG0L?2nRUFj#aLr-R1fX*$sXhad)~xdA*=hF3zPZhha<2O$Ps+F07w*3#MTe?)T8|A!P!v+a|ot{|^$q(TX`35O{WI0RbU zCj?hgOv=Z)xV?F`@HKI11IKtT^ocP78cqHU!YS@cHI@{fPD?YXL)?sD~9thOAv4JM|K8OlQhPXgnevF=F7GKD2#sZW*d za}ma31wLm81IZxX(W#A9mBvLZr|PoLnP>S4BhpK8{YV_}C|p<)4#yO{#ISbco92^3 zv&kCE(q9Wi;9%7>>PQ!zSkM%qqqLZW7O`VXvcj;WcJ`2~v?ZTYB@$Q&^CTfvy?1r^ z;Cdi+PTtmQwHX_7Kz?r#1>D zS5lWU(Mw_$B&`ZPmqxpIvK<~fbXq?x20k1~9az-Q!uR78mCgRj*eQ>zh3c$W}>^+w^dIr-u{@s30J=)1zF8?Wn|H`GS<=>Om|DjzC{}Jt?{!fSJe*@$H zg>wFnlT)k#T?LslW zu$^7Uy~$SQ21cE?3Ijl+bLfuH^U5P^$@~*UY#|_`uvAIe(+wD2eF}z_y!pvomuVO; zS^9fbdv)pcm-B@CW|Upm<7s|0+$@@<&*>$a{aW+oJ%f+VMO<#wa)7n|JL5egEgoBv zl$BY(NQjE0#*nv=!kMnp&{2Le#30b)Ql2e!VkPLK*+{jv77H7)xG7&=aPHL7LK9ER z5lfHxBI5O{-3S?GU4X6$yVk>lFn;ApnwZybdC-GAvaznGW-lScIls-P?Km2mF>%B2 zkcrXTk+__hj-3f48U%|jX9*|Ps41U_cd>2QW81Lz9}%`mTDIhE)jYI$q$ma7Y-`>% z8=u+Oftgcj%~TU}3nP8&h7k+}$D-CCgS~wtWvM|UU77r^pUw3YCV80Ou*+bH0!mf0 zxzUq4ed6y>oYFz7+l18PGGzhB^pqSt)si=9M>~0(Bx9*5r~W7sa#w+_1TSj3Jn9mW zMuG9BxN=}4645Cpa#SVKjFst;9UUY@O<|wpnZk$kE+to^4!?0@?Cwr3(>!NjYbu?x z1!U-?0_O?k!NdM^-rIQ8p)%?M+2xkhltt*|l=%z2WFJhme7*2xD~@zk#`dQR$6Lmd zb3LOD4fdt$Cq>?1<%&Y^wTWX=eHQ49Xl_lFUA(YQYHGHhd}@!VpYHHm=(1-O=yfK#kKe|2Xc*9}?BDFN zD7FJM-AjVi)T~OG)hpSWqH>vlb41V#^G2B_EvYlWhDB{Z;Q9-0)ja(O+By`31=biA zG&Fs#5!%_mHi|E4Nm$;vVQ!*>=_F;ZC=1DTPB#CICS5fL2T3XmzyHu?bI;m7D4@#; ztr~;dGYwb?m^VebuULtS4lkC_7>KCS)F@)0OdxZIFZp@FM_pHnJes8YOvwB|++#G( z&dm*OP^cz95Wi15vh`Q+yB>R{8zqEhz5of>Po$9LNE{xS<)lg2*roP*sQ}3r3t<}; zPbDl{lk{pox~2(XY5=qg0z!W-x^PJ`VVtz$git7?)!h>`91&&hESZy1KCJ2nS^yMH z!=Q$eTyRi68rKxdDsdt+%J_&lapa{ds^HV9Ngp^YDvtq&-Xp}60B_w@Ma>_1TTC;^ zpbe!#gH}#fFLkNo#|`jcn?5LeUYto%==XBk6Ik0kc4$6Z+L3x^4=M6OI1=z5u#M%0 z0E`kevJEpJjvvN>+g`?gtnbo$@p4VumliZV3Z%CfXXB&wPS^5C+7of2tyVkMwNWBiTE2 z8CdPu3i{*vR-I(NY5syRR}I1TJOV@DJy-Xmvxn^IInF>Tx2e)eE9jVSz69$6T`M9-&om!T+I znia!ZWJRB28o_srWlAxtz4VVft8)cYloIoVF=pL zugnk@vFLXQ_^7;%hn9x;Vq?lzg7%CQR^c#S)Oc-8d=q_!2ZVH764V z!wDKSgP}BrVV6SfCLZnYe-7f;igDs9t+K*rbMAKsp9L$Kh<6Z;e7;xxced zn=FGY<}CUz31a2G}$Q(`_r~75PzM4l_({Hg&b@d8&jC}B?2<+ed`f#qMEWi z`gm!STV9E4sLaQX+sp5Nu9*;9g12naf5?=P9p@H@f}dxYprH+3ju)uDFt^V{G0APn zS;16Dk{*fm6&BCg#2vo?7cbkkI4R`S9SSEJ=#KBk3rl69SxnCnS#{*$!^T9UUmO#&XXKjHKBqLdt^3yVvu8yn|{ zZ#%1CP)8t-PAz(+_g?xyq;C2<9<5Yy<~C74Iw(y>uUL$+$mp(DRcCWbCKiGCZw@?_ zdomfp+C5xt;j5L@VfhF*xvZdXwA5pcdsG>G<8II-|1dhAgzS&KArcb0BD4ZZ#WfiEY{hkCq5%z9@f|!EwTm;UEjKJsUo696V>h zy##eXYX}GUu%t{Gql8vVZKkNhQeQ4C%n|RmxL4ee5$cgwlU+?V7a?(jI#&3wid+Kz5+x^G!bb#$q>QpR#BZ}Xo5UW^ zD&I`;?(a}Oys7-`I^|AkN?{XLZNa{@27Dv^s4pGowuyhHuXc zuctKG2x0{WCvg_sGN^n9myJ}&FXyGmUQnW7fR$=bj$AHR88-q$D!*8MNB{YvTTEyS zn22f@WMdvg5~o_2wkjItJN@?mDZ9UUlat2zCh(zVE=dGi$rjXF7&}*sxac^%HFD`Y zTM5D3u5x**{bW!68DL1A!s&$2XG@ytB~dX-?BF9U@XZABO`a|LM1X3HWCllgl0+uL z04S*PX$%|^WAq%jkzp~%9HyYIF{Ym?k)j3nMwPZ=hlCg9!G+t>tf0o|J2%t1 ztC+`((dUplgm3`+0JN~}&FRRJ3?l*>Y&TfjS>!ShS`*MwO{WIbAZR#<%M|4c4^dY8 z{Rh;-!qhY=dz5JthbWoovLY~jNaw>%tS4gHVlt5epV8ekXm#==Po$)}mh^u*cE>q7*kvX&gq)(AHoItMYH6^s6f(deNw%}1=7O~bTHSj1rm2|Cq+3M z93djjdomWCTCYu!3Slx2bZVy#CWDozNedIHbqa|otsUl+ut?>a;}OqPfQA05Yim_2 zs@^BjPoFHOYNc6VbNaR5QZfSMh2S*`BGwcHMM(1@w{-4jVqE8Eu0Bi%d!E*^Rj?cR z7qgxkINXZR)K^=fh{pc0DCKtrydVbVILI>@Y0!Jm>x-xM!gu%dehm?cC6ok_msDVA*J#{75%4IZt}X|tIVPReZS#aCvuHkZxc zHVMtUhT(wp09+w9j9eRqz~LtuSNi2rQx_QgQ(}jBt7NqyT&ma61ldD(s9x%@q~PQl zp6N*?=N$BtvjQ_xIT{+vhb1>{pM0Arde0!X-y))A4znDrVx8yrP3B1(7bKPE5jR@5 zwpzwT4cu~_qUG#zYMZ_!2Tkl9zP>M%cy>9Y(@&VoB84#%>amTAH{(hL4cDYt!^{8L z645F>BWO6QaFJ-{C-i|-d%j7#&7)$X7pv#%9J6da#9FB5KyDhkA+~)G0^87!^}AP>XaCSScr;kL;Z%RSPD2CgoJ;gpYT5&6NUK$86$T?jRH=w8nI9Z534O?5fk{kd z`(-t$8W|#$3>xoMfXvV^-A(Q~$8SKDE^!T;J+rQXP71XZ(kCCbP%bAQ1|%$%Ov9_a zyC`QP3uPvFoBqr_+$HenHklqyIr>PU_Fk5$2C+0eYy^~7U&(!B&&P2%7#mBUhM!z> z_B$Ko?{Pf6?)gpYs~N*y%-3!1>o-4;@1Zz9VQHh)j5U1aL-Hyu@1d?X;jtDBNk*vMXPn@ z+u@wxHN*{uHR!*g*4Xo&w;5A+=Pf9w#PeZ^x@UD?iQ&${K2c}UQgLRik-rKM#Y5rdDphdcNTF~cCX&9ViRP}`>L)QA4zNXeG)KXFzSDa6 zd^St;inY6J_i=5mcGTx4_^Ys`M3l%Q==f>{8S1LEHn{y(kbxn5g1ezt4CELqy)~TV6{;VW>O9?5^ ztcoxHRa0jQY7>wwHWcxA-BCwzsP>63Kt&3fy*n#Cha687CQurXaRQnf5wc9o8v7Rw zNwGr2fac;Wr-Ldehn7tF^(-gPJwPt@VR1f;AmKgxN&YPL;j=0^xKM{!wuU|^mh3NE zy35quf}MeL!PU;|{OW_x$TBothLylT-J>_x6p}B_jW1L>k)ps6n%7Rh z96mPkJIM0QFNYUM2H}YF5bs%@Chs6#pEnloQhEl?J-)es!(SoJpEPoMTdgA14-#mC zghayD-DJWtUu`TD8?4mR)w5E`^EHbsz2EjH5aQLYRcF{l7_Q5?CEEvzDo(zjh|BKg z3aJl_n#j&eFHsUw4~lxqnr!6NL*se)6H=A+T1e3xUJGQrd}oSPwSy5+$tt{2t5J5@(lFxl43amsARG74iyNC}uuS zd2$=(r6RdamdGx^eatX@F2D8?U23tDpR+Os?0Gq2&^dF+$9wiWf?=mDWfjo4LfRwL zI#SRV9iSz>XCSgEj!cW&9H-njJopYiYuq|2w<5R2!nZ27DyvU4UDrHpoNQZiGPkp@ z1$h4H46Zn~eqdj$pWrv;*t!rTYTfZ1_bdkZmVVIRC21YeU$iS-*XMNK`#p8Z_DJx| zk3Jssf^XP7v0X?MWFO{rACltn$^~q(M9rMYoVxG$15N;nP)A98k^m3CJx8>6}NrUd@wp-E#$Q0uUDQT5GoiK_R{ z<{`g;8s>UFLpbga#DAf%qbfi`WN1J@6IA~R!YBT}qp%V-j!ybkR{uY0X|x)gmzE0J z&)=eHPjBxJvrZSOmt|)hC+kIMI;qgOnuL3mbNR0g^<%|>9x7>{}>a2qYSZAGPt4it?8 zNcLc!Gy0>$jaU?}ZWxK78hbhzE+etM`67*-*x4DN>1_&{@5t7_c*n(qz>&K{Y?10s zXsw2&nQev#SUSd|D8w7ZD2>E<%g^; zV{yE_O}gq?Q|zL|jdqB^zcx7vo(^})QW?QKacx$yR zhG|XH|8$vDZNIfuxr-sYFR{^csEI*IM#_gd;9*C+SysUFejP0{{z7@P?1+&_o6=7V|EJLQun^XEMS)w(=@eMi5&bbH*a0f;iC~2J74V2DZIlLUHD&>mlug5+v z6xBN~8-ovZylyH&gG#ptYsNlT?-tzOh%V#Y33zlsJ{AIju`CjIgf$@gr8}JugRq^c zAVQ3;&uGaVlVw}SUSWnTkH_6DISN&k2QLMBe9YU=sA+WiX@z)FoSYX`^k@B!j;ZeC zf&**P?HQG6Rk98hZ*ozn6iS-dG}V>jQhb3?4NJB*2F?6N7Nd;EOOo;xR7acylLaLy z9)^lykX39d@8@I~iEVar4jmjjLWhR0d=EB@%I;FZM$rykBNN~jf>#WbH4U{MqhhF6 zU??@fSO~4EbU4MaeQ_UXQcFyO*Rae|VAPLYMJEU`Q_Q_%s2*>$#S^)&7er+&`9L=1 z4q4ao07Z2Vsa%(nP!kJ590YmvrWg+YrgXYs_lv&B5EcoD`%uL79WyYA$0>>qi6ov7 z%`ia~J^_l{p39EY zv>>b}Qs8vxsu&WcXEt8B#FD%L%ZpcVtY!rqVTHe;$p9rbb5O{^rFMB>auLn-^;s+-&P1#h~mf~YLg$8M9 zZ4#87;e-Y6x6QO<{McUzhy(%*6| z)`D~A(TJ$>+0H+mct(jfgL4x%^oC^T#u(bL)`E2tBI#V1kSikAWmOOYrO~#-cc_8! zCe|@1&mN2{*ceeiBldHCdrURk4>V}79_*TVP3aCyV*5n@jiNbOm+~EQ_}1#->_tI@ zqXv+jj2#8xJtW508rzFrYcJxoek@iW6SR@1%a%Bux&;>25%`j3UI`0DaUr7l79`B1 zqqUARhW1^h6=)6?;@v>xrZNM;t}{yY3P@|L}ey@gG( z9r{}WoYN(9TW&dE2dEJIXkyHA4&pU6ki=rx&l2{DLGbVmg4%3Dlfvn!GB>EVaY_%3+Df{fBiqJV>~Xf8A0aqUjgpa} zoF8YXO&^_x*Ej}nw-$-F@(ddB>%RWoPUj?p8U{t0=n>gAI83y<9Ce@Q#3&(soJ{64 z37@Vij1}5fmzAuIUnXX`EYe;!H-yTVTmhAy;y8VZeB#vD{vw9~P#DiFiKQ|kWwGFZ z=jK;JX*A;Jr{#x?n8XUOLS;C%f|zj-7vXtlf_DtP7bpurBeX%Hjwr z4lI-2TdFpzkjgiv!8Vfv`=SP+s=^i3+N~1ELNWUbH|ytVu>EyPN_3(4TM^QE1swRo zoV7Y_g)a>28+hZG0e7g%@2^s>pzR4^fzR-El}ARTmtu!zjZLuX%>#OoU3}|rFjJg} zQ2TmaygxJ#sbHVyiA5KE+yH0LREWr%^C*yR|@gM$nK2P zo}M}PV0v))uJh&33N>#aU376@ZH79u(Yw`EQ2hM3SJs9f99+cO6_pNW$j$L-CtAfe zYfM)ccwD!P%LiBk!eCD?fHCGvgMQ%Q2oT_gmf?OY=A>&PaZQOq4eT=lwbaf}33LCH zFD|)lu{K7$8n9gX#w4~URjZxWm@wlH%oL#G|I~Fb-v^0L0TWu+`B+ZG!yII)w05DU z>GO?n(TN+B=>HdxVDSlIH76pta$_LhbBg;eZ`M7OGcqt||qi zogS72W1IN%=)5JCyOHWoFP7pOFK0L*OAh=i%&VW&4^LF@R;+K)t^S!96?}^+5QBIs zjJNTCh)?)4k^H^g1&jc>gysM`y^8Rm3qsvkr$9AeWwYpa$b22=yAd1t<*{ zaowSEFP+{y?Ob}8&cwfqoy4Pb9IA~VnM3u!trIK$&&0Op#Ql4j>(EW?UNUv#*iH1$ z^j>+W{afcd`{e&`-A{g}{JnIzYib)!T56IT@YEs{4|`sMpW3c8@UCoIJv`XsAw!XC z34|Il$LpW}CIHFC5e*)}00I5{%OL*WZRGzC0?_}-9{#ue?-ug^ zLE|uv-~6xnSs_2_&CN9{9vyc!Xgtn36_g^wI0C4s0s^;8+p?|mm;Odt3`2ZjwtK;l zfd6j)*Fr#53>C6Y8(N5?$H0ma;BCF3HCjUs7rpb2Kf*x3Xcj#O8mvs#&33i+McX zQpBxD8!O{5Y8D&0*QjD=Yhl9%M0)&_vk}bmN_Ud^BPN;H=U^bn&(csl-pkA+GyY0Z zKV7sU_4n;}uR78ouo8O%g*V;79KY?3d>k6%gpcmQsKk&@Vkw9yna_3asGt`0Hmj59 z%0yiF*`jXhByBI9QsD=+>big5{)BGe&+U2gAARGe3ID)xrid~QN_{I>k}@tzL!Md_ z&=7>TWciblF@EMC3t4-WX{?!m!G6$M$1S?NzF*2KHMP3Go4=#ZHkeIv{eEd;s-yD# z_jU^Ba06TZqvV|Yd;Z_sN%$X=!T+&?#p+OQIHS%!LO`Hx0q_Y0MyGYFNoM{W;&@0@ zLM^!X4KhdtsET5G<0+|q0oqVXMW~-7LW9Bg}=E$YtNh1#1D^6Mz(V9?2g~I1( zoz9Cz=8Hw98zVLwC2AQvp@pBeKyidn6Xu0-1SY1((^Hu*-!HxFUPs)yJ+i`^BC>PC zjwd0mygOVK#d2pRC9LxqGc6;Ui>f{YW9Bvb>33bp^NcnZoH~w9(lM5@JiIlfa-6|k ziy31UoMN%fvQfhi8^T+=yrP{QEyb-jK~>$A4SZT-N56NYEbpvO&yUme&pWKs3^94D zH{oXnUTb3T@H+RgzML*lejx`WAyw*?K7B-I(VJx($2!NXYm%3`=F~TbLv3H<{>D?A zJo-FDYdSA-(Y%;4KUP2SpHKAIcv9-ld(UEJE7=TKp|Gryn;72?0LHqAN^fk6%8PCW z{g_-t)G5uCIf0I`*F0ZNl)Z>))MaLMpXgqWgj-y;R+@A+AzDjsTqw2Mo9ULKA3c70 z!7SOkMtZb+MStH>9MnvNV0G;pwSW9HgP+`tg}e{ij0H6Zt5zJ7iw`hEnvye!XbA@!~#%vIkzowCOvq5I5@$3wtc*w2R$7!$*?}vg4;eDyJ_1=ixJuEp3pUS27W?qq(P^8$_lU!mRChT}ctvZz4p!X^ zOSp|JOAi~f?UkwH#9k{0smZ7-#=lK6X3OFEMl7%)WIcHb=#ZN$L=aD`#DZKOG4p4r zwlQ~XDZ`R-RbF&hZZhu3(67kggsM-F4Y_tI^PH8PMJRcs7NS9ogF+?bZB*fcpJ z=LTM4W=N9yepVvTj&Hu~0?*vR1HgtEvf8w%Q;U0^`2@e8{SwgX5d(cQ|1(!|i$km! zvY03MK}j`sff;*-%mN~ST>xU$6Bu?*Hm%l@0dk;j@%>}jsgDcQ)Hn*UfuThz9(ww_ zasV`rSrp_^bp-0sx>i35FzJwA!d6cZ5#5#nr@GcPEjNnFHIrtUYm1^Z$;{d&{hQV9 z6EfFHaIS}46p^5I-D_EcwwzUUuO}mqRh&T7r9sfw`)G^Q%oHxEs~+XoM?8e*{-&!7 z7$m$lg9t9KP9282eke608^Q2E%H-xm|oJ8=*SyEo} z@&;TQ3K)jgspgKHyGiKVMCz>xmC=H5Fy3!=TP)-R3|&1S-B)!6q50wfLHKM@7Bq6E z44CY%G;GY>tC`~yh!qv~YdXw! zSkquvYNs6k1r7>Eza?Vkkxo6XRS$W7EzL&A`o>=$HXgBp{L(i^$}t`NcnAxzbH8Ht z2!;`bhKIh`f1hIFcI5bHI=ueKdzmB9)!z$s-BT4ItyY|NaA_+o=jO%MU5as9 zc2)aLP>N%u>wlaXTK!p)r?+~)L+0eCGb5{8WIk7K52$nufnQ+m8YF+GQc&{^(zh-$ z#wyWV*Zh@d!b(WwXqvfhQX)^aoHTBkc;4ossV3&Ut*k>AI|m+{#kh4B!`3*<)EJVj zwrxK>99v^k4&Y&`Awm>|exo}NvewV%E+@vOc>5>%H#BK9uaE2$vje zWYM5fKuOTtn96B_2~~!xJPIcXF>E_;yO8AwpJ4)V`Hht#wbO3Ung~@c%%=FX4)q+9 z99#>VC2!4l`~0WHs9FI$Nz+abUq# zz`Of97})Su=^rGp2S$)7N3rQCj#0%2YO<R&p>$<#lgXcUj=4H_{oAYiT3 z44*xDn-$wEzRw7#@6aD)EGO$0{!C5Z^7#yl1o;k0PhN=aVUQu~eTQ^Xy{z8Ow6tk83 z4{5xe%(hx)%nD&|e*6sTWH`4W&U!Jae#U4TnICheJmsw{l|CH?UA{a6?2GNgpZLyzU2UlFu1ZVwlALmh_DOs03J^Cjh1im`E3?9&zvNmg(MuMw&0^Lu$(#CJ*q6DjlKsY-RMJ^8yIY|{SQZ*9~CH|u9L z`R78^r=EbbR*_>5?-)I+$6i}G)%mN(`!X72KaV(MNUP7Nv3MS9S|Pe!%N2AeOt5zG zVJ;jI4HZ$W->Ai_4X+`9c(~m=@ek*m`ZQbv3ryI-AD#AH=`x$~WeW~M{Js57(K7(v ze5`};LG|%C_tmd>bkufMWmAo&B+DT9ZV~h(4jg0>^aeAqL`PEUzJJtI8W1M!bQWpv zvN(d}E1@nlYa!L!!A*RN!(Q3F%J?5PvQ0udu?q-T)j3JKV~NL>KRb~w-lWc685uS6 z=S#aR&B8Sc8>cGJ!!--?kwsJTUUm`Jk?7`H z7PrO~xgBrSW2_tTlCq1LH8*!o?pj?qxy8}(=r_;G18POrFh#;buWR0qU24+XUaVZ0 z?(sXcr@-YqvkCmHr{U2oPogHL{r#3r49TeR<{SJX1pcUqyWPrkYz^X8#QW~?F)R5i z>p^!i<;qM8Nf{-fd6!_&V*e_9qP6q(s<--&1Ttj01j0w>bXY7y1W*%Auu&p|XSOH=)V7Bd4fUKh&T1)@cvqhuD-d=?w}O zjI%i(f|thk0Go*!d7D%0^ztBfE*V=(ZIN84f5HU}T9?ulmEYzT5usi=DeuI*d|;M~ zp_=Cx^!4k#=m_qSPBr5EK~E?3J{dWWPH&oCcNepYVqL?nh4D5ynfWip$m*YlZ8r^Z zuFEUL-nW!3qjRCLIWPT0x)FDL7>Yt7@8dA?R2kF@WE>ysMY+)lTsgNM#3VbXVGL}F z1O(>q>2a+_`6r5Xv$NZAnp=Kgnr3)cL(^=8ypEeOf3q8(HGe@7Tt59;yFl||w|mnO zHDxg2G3z8=(6wjj9kbcEY@Z0iOd7Gq5GiPS5% z*sF1J<#daxDV2Z8H>wxOF<;yKzMeTaSOp_|XkS9Sfn6Mpe9UBi1cSTieGG5$O;ZLIIJ60Y>SN4vC?=yE_CWlo(EEE$e4j?z&^FM%kNmRtlbEL^dPPgvs9sbK5fGw*r@ z+!EU@u$T8!nZh?Fdf_qk$VuHk^yVw`h`_#KoS*N%epIIOfQUy_&V}VWDGp3tplMbf z5Se1sJUC$7N0F1-9jdV2mmGK{-}fu|Nv;12jDy0<-kf^AmkDnu6j~TPWOgy1MT68|D z=4=50jVbUKdKaQgD`eWGr3I&^<6uhkjz$YwItY8%Yp9{z4-{6g{73<_b*@XJ4Nm3-3z z?BW3{aY_ccRjb@W1)i5nLg|7BnWS!B`_Uo9CWaE`Ij327QH?i)9A}4Ug4wmxVVa^b z-4+m%-wwOl7cKH7+=x&nrCrbEC)Q$fpg&V83#uEH;C=GNMz`ps@^RxK%T*8%OPnC` z{WO~J%nxYJ`x|N%?&i7?;{_8t^jM&=50HlaOQj8fS}_`moH$c;vI<|cruPFnpT8yU zS%rPOCUSd5Zdb(zwk`hqwTQn)*&n)uYsP*F_(~xEWq}C= zv30kFmZFwJZ@ELVX3?$dXQh|icO7UrL*_5G=I^xXjImz`ZPp>?g#tf(ej~KaIU0algsG!IS09;>?MvqGg#c{i+}qY|{P8W~O%#>|gFd z<1dr$-oxyRGN17yZo1OwLnzwYs0|;IS_nymNB0IlSzPQ%-r`?T=;_XQ^~&#}b|AB} zkNbN5uB?-sUB-T5QLlg%Uk3)uHB;>VIzGe9_J9 zaeISkQm!v(9d(0ML^b9fR^sfHFlH?7Mvddt37OuR{|O0{uv)(&-6<87W4 zyO>s!=cPgP3O&7xxU5DlIPw_o3O>6o6Qb?JWs3qw#p3sBc3g$?Dx zi(6D+DYgV;GrUis-CL%Qe{nvZnwaVXmbhH(|GFh|Q)k=1uvA$I@1DXI7bKlQ@8D6P zS?(*?><>)G49q0wr;NajpxP4W2G)kHl6^=Z>hrNEI4Mwd_$O6$1dXF;Q#hE(-eeW6 zz03GJF%Wl?HO=_ztv5*zRlcU~{+{k%#N59mgm~eK>P!QZ6E?#Cu^2)+K8m@ySvZ*5 z|HDT}BkF@3!l(0%75G=1u2hETXEj!^1Z$!)!lyGXlWD!_vqGE$Z)#cUVBqlORW>0^ zDjyVTxwKHKG|0}j-`;!R-p>}qQfBl(?($7pP<+Y8QE#M8SCDq~k<+>Q^Zf@cT_WdX3~BSe z+|KK|7OL5Hm5(NFP~j>Ct3*$wi0n0!xl=(C61`q&cec@mFlH(sy%+RH<=s)8aAPN`SfJdkAQjdv82G5iRdv8 zh{9wHUZaniSEpslXl^_ODh}mypC?b*9FzLjb~H@3DFSe;D(A-K3t3eOTB(m~I6C;(-lKAvit(70k`%@+O*Ztdz;}|_TS~B?Tpmi=QKC^m_ z2YpEaT3iiz*;T~ap1yiA)a`dKMwu`^UhIUeltNQ1Yjo=q@bI@&3zH?rVUg=IxLy-ni zyxDu%-Fr{H6owTjZU2O5>nDb=q&Jz_TjeSq%!2m40x&U6w~GQ({quPL73IsJS;f`$ zsuhioqCBj(gJ>2hoo)Gou7(WP*pX)f=Y=!=k!&1K?EYY%jJ~X&DnK{^saPQK<1BJ z_A`_{%ZozcB(3w$z^To^6d|XuT@=X~wtW!+{4ID@N{AB~J6AL5vuY>JwvWCNFKsKh zd}@>q@_WV#QZ&UJ0#?X(pXR!oyXOEG3rqzHbCzGLONDb042i$})fM@XF)uSP(DHUc z^&{|$*xe{cs?Gp8=B%RY3L7#$ve$?TWh>MZdxF1zH1v}1z+$Ov#G7?%D)bBCyDe*% zSeKSpETC2V1){II>@UwJi>4uBN+iAx+82E~gb|Cr&8E^i&)A!uv-g?jzH99wU}8+# z$nh>yvb;TwZmS@7LrvuCu_d0-WxFNI&C7%sWuTL%YU!l|I1{|->=dlOeHOCtUO#zkS3ESO8LHV4hTdQL5EdV zuWD33fFPH}HPrW^s$Qn1Xgp&AT6<-He{{4%eIu3rN=iK|9mURdKXfB&Q?qGok%!cs ze53UP{Z!TO-Y@q2;;k2avA3`lm4OoN4@S*k=UA)7H;qZ`d8`XaYFCv?Ba+uGW@r5v z&&{nf(24WSBOhc7!qF^@0cz;XcUynNaj6w2349;s!K{KVqs5yS{ z7VubS`2OzT^5#1~6Tt^RTvt9-J|D2F>y~>2;jeF>g`hx5l%B3H=aLExQihuYngzlnBTYOTHJQMzl>kwqN5JYs)Ej zblA@ntkUS~xi+}y6|(81helS}Q~&VB37qyV|S3Y=><^1wh%msQM?fz z<58MX(=|PSUKCF#)dbhR%D&xgCD?$aR0qen+wpp6 zst}vX18!Be96TD??j1HsHTUx(a&@F?=gT`Q$oJFFyrh^;zgz!(NlAHGn0cJy@us=w zNhC#l5G;H}+>49Nsh12=ZPO2r*2OBQe5kpb&1?*PIBFitK8}FUfb~S-#hKfF0o#&d z#3aPkB$9scYku&kA6{0xHnBV#&Wei5J>5T-XX-gUXEPo+9b7WL=*XESc(3BshL`aj zXp}QIp*40}oWJt*l043e8_5;H5PI5c)U&IEw5dF(4zjX0y_lk9 zAp@!mK>WUqHo)-jop=DoK>&no>kAD=^qIE7qis&_*4~ z6q^EF$D@R~3_xseCG>Ikb6Gfofb$g|75PPyyZN&tiRxqovo_k zO|HA|sgy#B<32gyU9x^&)H$1jvw@qp+1b(eGAb)O%O!&pyX@^nQd^9BQ4{(F8<}|A zhF&)xusQhtoXOOhic=8#Xtt5&slLia3c*a?dIeczyTbC#>FTfiLST57nc3@Y#v_Eg#VUv zT8cKH#f3=1PNj!Oroz_MAR*pow%Y0*6YCYmUy^7`^r|j23Q~^*TW#cU7CHf0eAD_0 zEWEVddxFgQ7=!nEBQ|ibaScslvhuUk^*%b#QUNrEB{3PG@uTxNwW}Bs4$nS9wc(~O zG7Iq>aMsYkcr!9#A;HNsJrwTDYkK8ikdj{M;N$sN6BqJ<8~z>T20{J8Z2rRUuH7~3 z=tgS`AgxbBOMg87UT4Lwge`*Y=01Dvk>)^{Iu+n6fuVX4%}>?3czOGR$0 zpp*wp>bsFFSV`V;r_m+TZns$ZprIi`OUMhe^cLE$2O+pP3nP!YB$ry}2THx2QJs3< za1;>d-AggCarrQ>&Z!d@;mW+!q6eXhb&`GbzUDSxpl8AJ#Cm#tuc)_xh(2NV=5XMs zrf_ozRYO$NkC=pKFX5OH8v1>0i9Z$ec`~Mf+_jQ68spn(CJwclDhEEkH2Qw;${J$clv__nUjn5jA0wCLEnu1j;v!0vB>Ri6m9`;R{JMS%^)4FC zU0Z44+u$I$w=Bj|iu4DT5h~sS`C*zbmX?@-crY}E+hy>}2~C0Nn(EKk@5^qO4@l@! z6O0lr%tzGC`D^)8xU3FnMZVm0kX1sBWhaQyzVoXFWwr%Ny?=2M{5s#5i7fTu3gEkG zc{(Pr$v=;`Y#&`y*J}#M9ux>0?xu!`$9cUKm#Bdd_&S#LPTS?ZPV6zN6>W6JTS~-LfjL{mB=b(KMk3 z2HjBSlJeyUVqDd=Mt!=hpYsvby2GL&3~zm;0{^nZJq+4vb?5HH4wufvr}IX42sHeK zm@x?HN$8TsTavXs)tLDFJtY9b)y~Tl@7z4^I8oUQq4JckH@~CVQ;FoK(+e0XAM>1O z(ei}h?)JQp>)d=6ng-BZF1Z5hsAKW@mXq+hU?r8I(*%`tnIIOXw7V6ZK(T9RFJJe@ zZS!aC+p)Gf2Ujc=a6hx4!A1Th%YH!Lb^xpI!Eu` zmJO{9rw){B1Ql18d%F%da+Tbu1()?o(zT7StYqK6_w`e+fjXq5L^y(0 z09QA6H4oFj59c2wR~{~>jUoDzDdKz}5#onYPJRwa`SUO)Pd4)?(ENBaFVLJr6Kvz= zhTtXqbx09C1z~~iZt;g^9_2nCZ{};-b4dQJbv8HsWHXPVg^@(*!@xycp#R?a|L!+` zY5w))JWV`Gls(=}shH0#r*;~>_+-P5Qc978+QUd>J%`fyn{*TsiG-dWMiJXNgwBaT zJ=wgYFt+1ACW)XwtNx)Q9tA2LPoB&DkL16P)ERWQlY4%Y`-5aM9mZ{eKPUgI!~J3Z zkMd5A_p&v?V-o-6TUa8BndiX?ooviev(DKw=*bBVOW|=zps9=Yl|-R5@yJe*BPzN}a0mUsLn{4LfjB_oxpv(mwq# zSY*%E{iB)sNvWfzg-B!R!|+x(Q|b@>{-~cFvdDHA{F2sFGA5QGiIWy#3?P2JIpPKg6ncI^)dvqe`_|N=8DQ>P;GjjD{w zH}lENr;dU&FbEU?00aa80D$0M0RRB{U*7-#kbjS|qAG&4l5%47zyJ#WrfA#1$1Ctx zf&Z_d{GW=lf^w2#qRJ|CvSJUi(^E3iv~=^Z(zH}F)3Z%V3`@+rNB7gTVU{Bb~90p|f+0(v;nz01EG7yDMX9@S~__vVgv%rS$+?IH+oZ03D5zYrv|^ zC1J)SruYHmCki$jLBlTaE5&dFG9-kq3!^i>^UQL`%gn6)jz54$WDmeYdsBE9;PqZ_ zoGd=P4+|(-u4U1dbAVQrFWoNgNd;0nrghPFbQrJctO>nwDdI`Q^i0XJDUYm|T|RWc zZ3^Qgo_Qk$%Fvjj-G}1NB#ZJqIkh;kX%V{THPqOyiq)d)0+(r9o(qKlSp*hmK#iIY zA^)Vr$-Hz<#SF=0@tL@;dCQsm`V9s1vYNq}K1B)!XSK?=I1)tX+bUV52$YQu*0%fnWEukW>mxkz+%3-S!oguE8u#MGzST8_Dy^#U?fA@S#K$S@9msUiX!gd_ow>08w5)nX{-KxqMOo7d?k2&?Vf z&diGDtZr(0cwPe9z9FAUSD9KC)7(n^lMWuayCfxzy8EZsns%OEblHFSzP=cL6}?J| z0U$H!4S_TVjj<`6dy^2j`V`)mC;cB%* z8{>_%E1^FH!*{>4a7*C1v>~1*@TMcLK{7nEQ!_igZC}ikJ$*<$yHy>7)oy79A~#xE zWavoJOIOC$5b6*q*F_qN1>2#MY)AXVyr$6x4b=$x^*aqF*L?vmj>Mgv+|ITnw_BoW zO?jwHvNy^prH{9$rrik1#fhyU^MpFqF2fYEt(;4`Q&XWOGDH8k6M=%@fics4ajI;st# zCU^r1CK&|jzUhRMv;+W~6N;u<;#DI6cCw-otsc@IsN3MoSD^O`eNflIoR~l4*&-%RBYk@gb^|-JXs&~KuSEmMxB}xSb z@K76cXD=Y|=I&SNC2E+>Zg?R6E%DGCH5J1nU!A|@eX9oS(WPaMm==k2s_ueCqdZw| z&hqHp)47`c{BgwgvY2{xz%OIkY1xDwkw!<0veB#yF4ZKJyabhyyVS`gZepcFIk%e2 zTcrmt2@-8`7i-@5Nz>oQWFuMC_KlroCl(PLSodswHqJ3fn<;gxg9=}~3x_L3P`9Sn zChIf}8vCHvTriz~T2~FamRi?rh?>3bX1j}%bLH+uFX+p&+^aXbOK7clZxdU~6Uxgy z8R=obwO4dL%pmVo*Ktf=lH6hnlz_5k3cG;m8lgaPp~?eD!Yn2kf)tU6PF{kLyn|oI@eQ`F z3IF7~Blqg8-uwUuWZScRKn%c2_}dXB6Dx_&xR*n9M9LXasJhtZdr$vBY!rP{c@=)& z#!?L$2UrkvClwQO>U*fSMs67oSj2mxiJ$t;E|>q%Kh_GzzWWO&3;ufU%2z%ucBU8H z3WIwr$n)cfCXR&>tyB7BcSInK>=ByZA%;cVEJhcg<#6N{aZC4>K41XF>ZgjG`z_u& zGY?;Ad?-sgiOnI`oppF1o1Gurqbi*;#x2>+SSV6|1^G@ooVy@fg?wyf@0Y!UZ4!}nGuLeC^l)6pwkh|oRY`s1Pm$>zZ3u-83T|9 zGaKJIV3_x+u1>cRibsaJpJqhcm%?0-L;2 zitBrdRxNmb0OO2J%Y&Ym(6*`_P3&&5Bw157{o7LFguvxC$4&zTy#U=W*l&(Q2MNO} zfaUwYm{XtILD$3864IA_nn34oVa_g^FRuHL5wdUd)+W-p-iWCKe8m_cMHk+=? zeKX)M?Dt(|{r5t7IenkAXo%&EXIb-i^w+0CX0D=xApC=|Xy(`xy+QG^UyFe z+#J6h_&T5i#sV)hj3D4WN%z;2+jJcZxcI3*CHXGmOF3^)JD5j&wfX)e?-|V0GPuA+ zQFot%aEqGNJJHn$!_}#PaAvQ^{3-Ye7b}rWwrUmX53(|~i0v{}G_sI9uDch_brX&6 zWl5Ndj-AYg(W9CGfQf<6!YmY>Ey)+uYd_JNXH=>|`OH-CDCmcH(0%iD_aLlNHKH z7bcW-^5+QV$jK?R*)wZ>r9t}loM@XN&M-Pw=F#xn(;u3!(3SXXY^@=aoj70;_=QE9 zGghsG3ekq#N||u{4We_25U=y#T*S{4I{++Ku)> zQ!DZW;pVcn>b;&g2;YE#+V`v*Bl&Y-i@X6D*OpNA{G@JAXho&aOk(_j^weW{#3X5Y z%$q_wpb07EYPdmyH(1^09i$ca{O<}7) zRWncXdSPgBE%BM#by!E>tdnc$8RwUJg1*x($6$}ae$e9Knj8gvVZe#bLi!<+&BkFj zg@nOpDneyc+hU9P-;jmOSMN|*H#>^Ez#?;%C3hg_65leSUm;iz)UkW)jX#p)e&S&M z1|a?wDzV5NVnlhRBCd_;F87wp>6c<&nkgvC+!@KGiIqWY4l}=&1w7|r6{oBN8xyzh zG$b#2=RJp_iq6)#t5%yLkKx(0@D=C3w+oiXtSuaQ%I1WIb-eiE$d~!)b@|4XLy!CZ z9p=t=%3ad@Ep+<9003D2KZ5VyP~_n$=;~r&YUg5UZ0KVD&tR1DHy9x)qWtKJp#Kq# zP*8p#W(8JJ_*h_3W}FlvRam?<4Z+-H77^$Lvi+#vmhL9J zJ<1SV45xi;SrO2f=-OB(7#iNA5)x1uNC-yNxUw|!00vcW2PufRm>e~toH;M0Q85MQLWd?3O{i8H+5VkR@l9Dg-ma ze2fZ%>G(u5(k9EHj2L6!;(KZ8%8|*-1V|B#EagbF(rc+5iL_5;Eu)L4Z-V;0HfK4d z*{utLse_rvHZeQ>V5H=f78M3Ntg1BPxFCVD{HbNA6?9*^YIq;B-DJd{Ca2L#)qWP? zvX^NhFmX?CTWw&Ns}lgs;r3i+Bq@y}Ul+U%pzOS0Fcv9~aB(0!>GT0)NO?p=25LjN z2bh>6RhgqD7bQj#k-KOm@JLgMa6>%-ok1WpOe)FS^XOU{c?d5shG(lIn3GiVBxmg`u%-j=)^v&pX1JecJics3&jvPI)mDut52? z3jEA)DM%}BYbxxKrizVYwq?(P&19EXlwD9^-6J+4!}9{ywR9Gk42jjAURAF&EO|~N z)?s>$Da@ikI4|^z0e{r`J8zIs>SpM~Vn^{3fArRu;?+43>lD+^XtUcY1HidJwnR6+ z!;oG2=B6Z_=M%*{z-RaHc(n|1RTKQdNjjV!Pn9lFt^4w|AeN06*j}ZyhqZ^!-=cyGP_ShV1rGxkx8t zB;8`h!S{LD%ot``700d0@Grql(DTt4Awgmi+Yr0@#jbe=2#UkK%rv=OLqF)9D7D1j z!~McAwMYkeaL$~kI~90)5vBhBzWYc3Cj1WI0RS`z000R8-@ET0dA~*r(gSiCJmQMN&4%1D zyVNf0?}sBH8zNbBLn>~(W{d3%@kL_eQ6jEcR{l>C|JK z(R-fA!z|TTRG40|zv}7E@PqCAXP3n`;%|SCQ|ZS%ym$I{`}t3KPL&^l5`3>yah4*6 zifO#{VNz3)?ZL$be;NEaAk9b#{tV?V7 zP|wf5YA*1;s<)9A4~l3BHzG&HH`1xNr#%){4xZ!jq%o=7nN*wMuXlFV{HaiQLJ`5G zBhDi#D(m`Q1pLh@Tq+L;OwuC52RdW7b8}~60WCOK5iYMUad9}7aWBuILb({5=z~YF zt?*Jr5NG+WadM{mDL>GyiByCuR)hd zA=HM?J6l1Xv0Dl+LW@w$OTcEoOda^nFCw*Sy^I@$sSuneMl{4ys)|RY#9&NxW4S)9 zq|%83IpslTLoz~&vTo!Ga@?rj_kw{|k{nv+w&Ku?fyk4Ki4I?);M|5Axm)t+BaE)D zm(`AQ#k^DWrjbuXoJf2{Aj^KT zFb1zMSqxq|vceV+Mf-)$oPflsO$@*A0n0Z!R{&(xh8s}=;t(lIy zv$S8x>m;vQNHuRzoaOo?eiWFe{0;$s`Bc+Osz~}Van${u;g(su`3lJ^TEfo~nERfP z)?aFzpDgnLYiERsKPu|0tq4l2wT)Atr6Qb%m-AUn6HnCue*yWICp7TjW$@sO zm5rm4aTcPQ(rfi7a`xP7cKCFrJD}*&_~xgLyr^-bmsL}y;A5P|al8J3WUoBSjqu%v zxC;mK!g(7r6RRJ852Z~feoC&sD3(6}^5-uLK8o)9{8L_%%rItZK9C){UxB|;G>JbP zsRRtS4-3B*5c+K2kvmgZK8472%l>3cntWUOVHxB|{Ay~aOg5RN;{PJgeVD*H%ac+y!h#wi%o2bF2Ca8IyMyH{>4#{E_8u^@+l-+n=V}Sq?$O z{091@v%Bd*3pk0^2UtiF9Z+(a@wy6 zUdw8J*ze$K#=$48IBi1U%;hmhO>lu!uU;+RS}p&6@rQila7WftH->*A4=5W|Fmtze z)7E}jh@cbmr9iup^i%*(uF%LG&!+Fyl@LFA-}Ca#bxRfDJAiR2dt6644TaYw1Ma79 zt8&DYj31j^5WPNf5P&{)J?WlCe@<3u^78wnd(Ja4^a>{^Tw}W>|Cjt^If|7l^l)^Q zbz|7~CF(k_9~n|h;ysZ+jHzkXf(*O*@5m zLzUmbHp=x!Q|!9NVXyipZ3)^GuIG$k;D)EK!a5=8MFLI_lpf`HPKl=-Ww%z8H_0$j ztJ||IfFG1lE9nmQ0+jPQy zCBdKkjArH@K7jVcMNz);Q(Q^R{d5G?-kk;Uu_IXSyWB)~KGIizZL(^&qF;|1PI7!E zTP`%l)gpX|OFn&)M%txpQ2F!hdA~hX1Cm5)IrdljqzRg!f{mN%G~H1&oqe`5eJCIF zHdD7O;AX-{XEV(a`gBFJ9ews#CVS2y!&>Cm_dm3C8*n3MA*e67(WC?uP@8TXuMroq z{#w$%z@CBIkRM7?}Xib+>hRjy?%G!fiw8! z8(gB+8J~KOU}yO7UGm&1g_MDJ$IXS!`+*b*QW2x)9>K~Y*E&bYMnjl6h!{17_8d!%&9D`a7r&LKZjC<&XOvTRaKJ1 zUY@hl5^R&kZl3lU3njk`3dPzxj$2foOL26r(9zsVF3n_F#v)s5vv3@dgs|lP#eylq62{<-vczqP!RpVBTgI>@O6&sU>W|do17+#OzQ7o5A$ICH z?GqwqnK^n2%LR;$^oZM;)+>$X3s2n}2jZ7CdWIW0lnGK-b#EG01)P@aU`pg}th&J-TrU`tIpb5t((0eu|!u zQz+3ZiOQ^?RxxK4;zs=l8q!-n7X{@jSwK(iqNFiRColuEOg}!7cyZi`iBX4g1pNBj zAPzL?P^Ljhn;1$r8?bc=#n|Ed7wB&oHcw()&*k#SS#h}jO?ZB246EGItsz*;^&tzp zu^YJ0=lwsi`eP_pU8}6JA7MS;9pfD;DsSsLo~ogzMNP70@@;Fm8f0^;>$Z>~}GWRw!W5J3tNX*^2+1f3hz{~rIzJo z6W%J(H!g-eI_J1>0juX$X4Cl6i+3wbc~k146UIX&G22}WE>0ga#WLsn9tY(&29zBvH1$`iWtTe zG2jYl@P!P)eb<5DsR72BdI7-zP&cZNI{7q3e@?N8IKc4DE#UVr->|-ryuJXk^u^>4 z$3wE~=q390;XuOQP~TNoDR?#|NSPJ%sTMInA6*rJ%go|=YjGe!B>z6u$IhgQSwoV* zjy3F2#I>uK{42{&IqP59)Y(1*Z>>#W8rCf4_eVsH)`v!P#^;BgzKDR`ARGEZzkNX+ zJUQu=*-ol=Xqqt5=`=pA@BIn@6a9G8C{c&`i^(i+BxQO9?YZ3iu%$$da&Kb?2kCCo zo7t$UpSFWqmydXf@l3bVJ=%K?SSw)|?srhJ-1ZdFu*5QhL$~-IQS!K1s@XzAtv6*Y zl8@(5BlWYLt1yAWy?rMD&bwze8bC3-GfNH=p zynNFCdxyX?K&G(ZZ)afguQ2|r;XoV^=^(;Cku#qYn4Lus`UeKt6rAlFo_rU`|Rq z&G?~iWMBio<78of-2X(ZYHx~=U0Vz4btyXkctMKdc9UM!vYr~B-(>)(Hc|D zMzkN4!PBg%tZoh+=Gba!0++d193gbMk2&krfDgcbx0jI92cq?FFESVg0D$>F+bil} zY~$)|>1HZsX=5sAZ2WgPB5P=8X#TI+NQ(M~GqyVB53c6IdX=k>Wu@A0Svf5#?uHaF zsYn|koIi3$(%GZ2+G+7Fv^lHTb#5b8sAHSTnL^qWZLM<(1|9|QFw9pnRU{svj}_Al zL)b9>fN{QiA($8peNEJyy`(a{&uh-T4_kdZFIVsKKVM(?05}76EEz?#W za^fiZOAd14IJ4zLX-n7Lq0qlQ^lW8Cvz4UKkV9~P}>sq0?xD3vg+$4vLm~C(+ zM{-3Z#qnZ09bJ>}j?6ry^h+@PfaD7*jZxBEY4)UG&daWb??6)TP+|3#Z&?GL?1i+280CFsE|vIXQbm| zM}Pk!U`U5NsNbyKzkrul-DzwB{X?n3E6?TUHr{M&+R*2%yOiXdW-_2Yd6?38M9Vy^ z*lE%gA{wwoSR~vN0=no}tP2Ul5Gk5M(Xq`$nw#ndFk`tcpd5A=Idue`XZ!FS>Q zG^0w#>P4pPG+*NC9gLP4x2m=cKP}YuS!l^?sHSFftZy{4CoQrb_ z^20(NnG`wAhMI=eq)SsIE~&Gp9Ne0nD4%Xiu|0Fj1UFk?6avDqjdXz{O1nKao*46y zT8~iA%Exu=G#{x=KD;_C&M+Zx4+n`sHT>^>=-1YM;H<72k>$py1?F3#T1*ef9mLZw z5naLQr?n7K;2l+{_uIw*_1nsTn~I|kkCgrn;|G~##hM;9l7Jy$yJfmk+&}W@JeKcF zx@@Woiz8qdi|D%aH3XTx5*wDlbs?dC1_nrFpm^QbG@wM=i2?Zg;$VK!c^Dp8<}BTI zyRhAq@#%2pGV49*Y5_mV4+OICP|%I(dQ7x=6Ob}>EjnB_-_18*xrY?b%-yEDT(wrO z9RY2QT0`_OpGfMObKHV;QLVnrK%mc?$WAdIT`kJQT^n%GuzE7|9@k3ci5fYOh(287 zuIbg!GB3xLg$YN=n)^pHGB0jH+_iIiC=nUcD;G6LuJsjn2VI1cyZx=a?ShCsF==QK z;q~*m&}L<-cb+mDDXzvvrRsybcgQ;Vg21P(uLv5I+eGc7o7tc6`;OA9{soHFOz zT~2?>Ts}gprIX$wRBb4yE>ot<8+*Bv`qbSDv*VtRi|cyWS>)Fjs>fkNOH-+PX&4(~ z&)T8Zam2L6puQl?;5zg9h<}k4#|yH9czHw;1jw-pwBM*O2hUR6yvHATrI%^mvs9q_ z&ccT0>f#eDG<^WG^q@oVqlJrhxH)dcq2cty@l3~|5#UDdExyXUmLQ}f4#;6fI{f^t zDCsgIJ~0`af%YR%Ma5VQq-p21k`vaBu6WE?66+5=XUd%Ay%D$irN>5LhluRWt7 zov-=f>QbMk*G##&DTQyou$s7UqjjW@k6=!I@!k+S{pP8R(2=e@io;N8E`EOB;OGoI zw6Q+{X1_I{OO0HPpBz!X!@`5YQ2)t{+!?M_iH25X(d~-Zx~cXnS9z>u?+If|iNJbx zyFU2d1!ITX64D|lE0Z{dLRqL1Ajj=CCMfC4lD3&mYR_R_VZ>_7_~|<^o*%_&jevU+ zQ4|qzci=0}Jydw|LXLCrOl1_P6Xf@c0$ieK2^7@A9UbF{@V_0p%lqW|L?5k>bVM8|p5v&2g;~r>B8uo<4N+`B zH{J)h;SYiIVx@#jI&p-v3dwL5QNV1oxPr8J%ooezTnLW>i*3Isb49%5i!&ac_dEXv zvXmVUck^QHmyrF8>CGXijC_R-y(Qr{3Zt~EmW)-nC!tiH`wlw5D*W7Pip;T?&j%kX z6DkZX4&}iw>hE(boLyjOoupf6JpvBG8}jIh!!VhnD0>}KSMMo{1#uU6kiFcA04~|7 zVO8eI&x1`g4CZ<2cYUI(n#wz2MtVFHx47yE5eL~8bot~>EHbevSt}LLMQX?odD{Ux zJMnam{d)W4da{l7&y-JrgiU~qY3$~}_F#G7|MxT)e;G{U`In&?`j<5D->}cb{}{T(4DF0BOk-=1195KB-E*o@c?`>y#4=dMtYtSY=&L{!TAjFVcq0y@AH`vH! z$41+u!Ld&}F^COPgL(EE{0X7LY&%D7-(?!kjFF7=qw<;`V{nwWBq<)1QiGJgUc^Vz ztMUlq1bZqKn17|6x6iAHbWc~l1HcmAxr%$Puv!znW)!JiukwIrqQ00|H$Z)OmGG@= zv%A8*4cq}(?qn4rN6o`$Y))(MyXr8R<2S^J+v(wmFmtac!%VOfN?&(8Nr!T@kV`N; z*Q33V3t`^rN&aBiHet)18wy{*wi1=W!B%B-Q6}SCrUl$~Hl{@!95ydml@FK8P=u4s z4e*7gV2s=YxEvskw2Ju!2%{8h01rx-3`NCPc(O zH&J0VH5etNB2KY6k4R@2Wvl^Ck$MoR3=)|SEclT2ccJ!RI9Nuter7u9@;sWf-%um;GfI!=eEIQ2l2p_YWUd{|6EG ze{yO6;lMc>;2tPrsNdi@&1K6(1;|$xe8vLgiouj%QD%gYk`4p{Ktv9|j+!OF-P?@p z;}SV|oIK)iwlBs+`ROXkhd&NK zzo__r!B>tOXpBJMDcv!Mq54P+n4(@dijL^EpO1wdg~q+!DT3lB<>9AANSe!T1XgC=J^)IP0XEZ()_vpu!!3HQyJhwh?r`Ae%Yr~b% zO*NY9t9#qWa@GCPYOF9aron7thfWT`eujS4`t2uG6)~JRTI;f(ZuoRQwjZjp5Pg34 z)rp$)Kr?R+KdJ;IO;pM{$6|2y=k_siqvp%)2||cHTe|b5Ht8&A{wazGNca zX$Ol?H)E_R@SDi~4{d-|8nGFhZPW;Cts1;08TwUvLLv&_2$O6Vt=M)X;g%HUr$&06 zISZb(6)Q3%?;3r~*3~USIg=HcJhFtHhIV(siOwV&QkQe#J%H9&E21!C*d@ln3E@J* zVqRO^<)V^ky-R|%{(9`l-(JXq9J)1r$`uQ8a}$vr9E^nNiI*thK8=&UZ0dsFN_eSl z(q~lnD?EymWLsNa3|1{CRPW60>DSkY9YQ;$4o3W7Ms&@&lv9eH!tk~N&dhqX&>K@} zi1g~GqglxkZ5pEFkllJ)Ta1I^c&Bt6#r(QLQ02yHTaJB~- zCcE=5tmi`UA>@P=1LBfBiqk)HB4t8D?02;9eXj~kVPwv?m{5&!&TFYhu>3=_ zsGmYZ^mo*-j69-42y&Jj0cBLLEulNRZ9vXE)8~mt9C#;tZs;=#M=1*hebkS;7(aGf zcs7zH(I8Eui9UU4L--))yy`&d&$In&VA2?DAEss4LAPCLd>-$i?lpXvn!gu^JJ$(DoUlc6wE98VLZ*z`QGQov5l4Fm_h?V-;mHLYDVOwKz7>e4+%AzeO>P6v}ndPW| zM>m#6Tnp7K?0mbK=>gV}=@k*0Mr_PVAgGMu$j+pWxzq4MAa&jpCDU&-5eH27Iz>m^ zax1?*HhG%pJ((tkR(V(O(L%7v7L%!_X->IjS3H5kuXQT2!ow(;%FDE>16&3r){!ex zhf==oJ!}YU89C9@mfDq!P3S4yx$aGB?rbtVH?sHpg?J5C->!_FHM%Hl3#D4eplxzQ zRA+<@LD%LKSkTk2NyWCg7u=$%F#;SIL44~S_OGR}JqX}X+=bc@swpiClB`Zbz|f!4 z7Ysah7OkR8liXfI`}IIwtEoL}(URrGe;IM8%{>b1SsqXh)~w}P>yiFRaE>}rEnNkT z!HXZUtxUp1NmFm)Dm@-{FI^aRQqpSkz}ZSyKR%Y}YHNzBk)ZIp} zMtS=aMvkgWKm9&oTcU0?S|L~CDqA+sHpOxwnswF-fEG)cXCzUR?ps@tZa$=O)=L+5 zf%m58cq8g_o}3?Bhh+c!w4(7AjxwQ3>WnVi<{{38g7yFboo>q|+7qs<$8CPXUFAN< zG&}BHbbyQ5n|qqSr?U~GY{@GJ{(Jny{bMaOG{|IkUj7tj^9pa9|FB_<+KHLxSxR;@ zHpS$4V)PP+tx}22fWx(Ku9y+}Ap;VZqD0AZW4gCDTPCG=zgJmF{|x;(rvdM|2|9a}cex6xrMkERnkE;}jvU-kmzd%_J50$M`lIPCKf+^*zL=@LW`1SaEc%=m zQ+lT06Gw+wVwvQ9fZ~#qd430v2HndFsBa9WjD0P}K(rZYdAt^5WQIvb%D^Q|pkVE^ zte$&#~zmULFACGfS#g=2OLOnIf2Of-k!(BIHjs77nr!5Q1*I9 z1%?=~#Oss!rV~?-6Gm~BWJiA4mJ5TY&iPm_$)H1_rTltuU1F3I(qTQ^U$S>%$l z)Wx1}R?ij0idp@8w-p!Oz{&*W;v*IA;JFHA9%nUvVDy7Q8woheC#|8QuDZb-L_5@R zOqHwrh|mVL9b=+$nJxM`3eE{O$sCt$UK^2@L$R(r^-_+z?lOo+me-VW=Zw z-Bn>$4ovfWd%SPY`ab-u9{INc*k2h+yH%toDHIyqQ zO68=u`N}RIIs7lsn1D){)~%>ByF<>i@qFb<-axvu(Z+6t7v<^z&gm9McRB~BIaDn$ z#xSGT!rzgad8o>~kyj#h1?7g96tOcCJniQ+*#=b7wPio>|6a1Z?_(TS{)KrPe}(8j z!#&A=k(&Pj^F;r)CI=Z{LVu>uj!_W1q4b`N1}E(i%;BWjbEcnD=mv$FL$l?zS6bW!{$7j1GR5ocn94P2u{ z70tAAcpqtQo<@cXw~@i-@6B23;317|l~S>CB?hR5qJ%J3EFgyBdJd^fHZu7AzHF(BQ!tyAz^L0`X z23S4Fe{2X$W0$zu9gm%rg~A>ijaE#GlYlrF9$ds^QtaszE#4M(OLVP2O-;XdT(XIC zatwzF*)1c+t~c{L=fMG8Z=k5lv>U0;C{caN1NItnuSMp)6G3mbahu>E#sj&oy94KC zpH}8oEw{G@N3pvHhp{^-YaZeH;K+T_1AUv;IKD<=mv^&Ueegrb!yf`4VlRl$M?wsl zZyFol(2|_QM`e_2lYSABpKR{{NlxlDSYQNkS;J66aT#MSiTx~;tUmvs-b*CrR4w=f z8+0;*th6kfZ3|5!Icx3RV11sp=?`0Jy3Fs0N4GZQMN=8HmT6%x9@{Dza)k}UwL6JT zHRDh;%!XwXr6yuuy`4;Xsn0zlR$k%r%9abS1;_v?`HX_hI|+EibVnlyE@3aL5vhQq zlIG?tN^w@0(v9M*&L+{_+RQZw=o|&BRPGB>e5=ys7H`nc8nx)|-g;s7mRc7hg{GJC zAe^vCIJhajmm7C6g! zL&!WAQ~5d_5)00?w_*|*H>3$loHrvFbitw#WvLB!JASO?#5Ig5$Ys10n>e4|3d;tS zELJ0|R4n3Az(Fl3-r^QiV_C;)lQ1_CW{5bKS15U|E9?ZgLec@%kXr84>5jV2a5v=w z?pB1GPdxD$IQL4)G||B_lI+A=08MUFFR4MxfGOu07vfIm+j=z9tp~5i_6jb`tR>qV z$#`=BQ*jpCjm$F0+F)L%xRlnS%#&gro6PiRfu^l!EVan|r3y}AHJQOORGx4~ z&<)3=K-tx518DZyp%|!EqpU!+X3Et7n2AaC5(AtrkW>_57i}$eqs$rupubg0a1+WO zGHZKLN2L0D;ab%{_S1Plm|hx8R?O14*w*f&2&bB050n!R2by zw!@XOQx$SqZ5I<(Qu$V6g>o#A!JVwErWv#(Pjx=KeS0@hxr4?13zj#oWwPS(7Ro|v z>Mp@Kmxo79q|}!5qtX2-O@U&&@6s~!I&)1WQIl?lTnh6UdKT_1R640S4~f=_xoN3- zI+O)$R@RjV$F=>Ti7BlnG1-cFKCC(t|Qjm{SalS~V-tX#+2ekRhwmN zZr`8{QF6y~Z!D|{=1*2D-JUa<(1Z=;!Ei!KiRNH?o{p5o3crFF=_pX9O-YyJchr$~ zRC`+G+8kx~fD2k*ZIiiIGR<8r&M@3H?%JVOfE>)})7ScOd&?OjgAGT@WVNSCZ8N(p zuQG~76GE3%(%h1*vUXg$vH{ua0b`sQ4f0*y=u~lgyb^!#CcPJa2mkSEHGLsnO^kb$ zru5_l#nu=Y{rSMWiYx?nO{8I!gH+?wEj~UM?IrG}E|bRIBUM>UlY<`T1EHpRr36vv zBi&dG8oxS|J$!zoaq{+JpJy+O^W(nt*|#g32bd&K^w-t>!Vu9N!k9eA8r!Xc{utY> zg9aZ(D2E0gL#W0MdjwES-7~Wa8iubPrd?8-$C4BP?*wok&O8+ykOx{P=Izx+G~hM8 z*9?BYz!T8~dzcZr#ux8kS7u7r@A#DogBH8km8Ry4slyie^n|GrTbO|cLhpqgMdsjX zJ_LdmM#I&4LqqsOUIXK8gW;V0B(7^$y#h3h>J0k^WJfAMeYek%Y-Dcb_+0zPJez!GM zAmJ1u;*rK=FNM0Nf}Y!!P9c4)HIkMnq^b;JFd!S3?_Qi2G#LIQ)TF|iHl~WKK6JmK zbv7rPE6VkYr_%_BT}CK8h=?%pk@3cz(UrZ{@h40%XgThP*-Oeo`T0eq9 zA8BnWZKzCy5e&&_GEsU4*;_k}(8l_&al5K-V*BFM=O~;MgRkYsOs%9eOY6s6AtE*<7GQAR2ulC3RAJrG_P1iQK5Z~&B z&f8X<>yJV6)oDGIlS$Y*D^Rj(cszTy5c81a5IwBr`BtnC6_e`ArI8CaTX_%rx7;cn zR-0?J_LFg*?(#n~G8cXut(1nVF0Oka$A$1FGcERU<^ggx;p@CZc?3UB41RY+wLS`LWFNSs~YP zuw1@DNN3lTd|jDL7gjBsd9}wIw}4xT2+8dBQzI00m<@?c2L%>}QLfK5%r!a-iII`p zX@`VEUH)uj^$;7jVUYdADQ2k*!1O3WdfgF?OMtUXNpQ1}QINamBTKDuv19^{$`8A1 zeq%q*O0mi@(%sZU>Xdb0Ru96CFqk9-L3pzLVsMQ`Xpa~N6CR{9Rm2)A|CI21L(%GW zh&)Y$BNHa=FD+=mBw3{qTgw)j0b!Eahs!rZnpu)z!!E$*eXE~##yaXz`KE5(nQM`s zD!$vW9XH)iMxu9R>r$VlLk9oIR%HxpUiW=BK@4U)|1WNQ=mz9a z^!KkO=>GaJ!GBXm{KJj^;kh-MkUlEQ%lza`-G&}C5y1>La1sR6hT=d*NeCnuK%_LV zOXt$}iP6(YJKc9j-Fxq~*ItVUqljQ8?oaysB-EYtFQp9oxZ|5m0^Hq(qV!S+hq#g( z?|i*H2MIr^Kxgz+3vIljQ*Feejy6S4v~jKEPTF~Qhq!(ms5>NGtRgO5vfPPc4Z^AM zTj!`5xEreIN)vaNxa|q6qWdg>+T`Ol0Uz)ckXBXEGvPNEL3R8hB3=C5`@=SYgAju1 z!)UBr{2~=~xa{b8>x2@C7weRAEuatC)3pkRhT#pMPTpSbA|tan%U7NGMvzmF?c!V8 z=pEWxbdXbTAGtWTyI?Fml%lEr-^AE}w#l(<7OIw;ctw}imYax&vR4UYNJZK6P7ZOd zP87XfhnUHxCUHhM@b*NbTi#(-8|wcv%3BGNs#zRCVV(W?1Qj6^PPQa<{yaBwZ`+<`w|;rqUY_C z&AeyKwwf*q#OW-F()lir=T^<^wjK65Lif$puuU5+tk$;e_EJ;Lu+pH>=-8=PDhkBg z8cWt%@$Sc#C6F$Vd+0507;{OOyT7Hs%nKS88q-W!$f~9*WGBpHGgNp}=C*7!RiZ5s zn1L_DbKF@B8kwhDiLKRB@lsXVVLK|ph=w%_`#owlf@s@V(pa`GY$8h%;-#h@TsO|Y8V=n@*!Rog7<7Cid%apR|x zOjhHCyfbIt%+*PCveTEcuiDi%Wx;O;+K=W?OFUV%)%~6;gl?<0%)?snDDqIvkHF{ zyI02)+lI9ov42^hL>ZRrh*HhjF9B$A@=H94iaBESBF=eC_KT$8A@uB^6$~o?3Wm5t1OIaqF^~><2?4e3c&)@wKn9bD? zoeCs;H>b8DL^F&>Xw-xjZEUFFTv>JD^O#1E#)CMBaG4DX9bD(Wtc8Rzq}9soQ8`jf zeSnHOL}<+WVSKp4kkq&?SbETjq6yr@4%SAqOG=9E(3YeLG9dtV+8vmzq+6PFPk{L; z(&d++iu=^F%b+ea$i2UeTC{R*0Isk;vFK!no<;L+(`y`3&H-~VTdKROkdyowo1iqR zbVW(3`+(PQ2>TKY>N!jGmGo7oeoB8O|P_!Ic@ zZ^;3dnuXo;WJ?S+)%P>{Hcg!Jz#2SI(s&dY4QAy_vRlmOh)QHvs_7c&zkJCmJGVvV zX;Mtb>QE+xp`KyciG$Cn*0?AK%-a|=o!+7x&&yzHQOS>8=B*R=niSnta^Pxp1`=md z#;$pS$4WCT?mbiCYU?FcHGZ#)kHVJTTBt^%XE(Q};aaO=Zik0UgLcc0I(tUpt(>|& zcxB_|fxCF7>&~5eJ=Dpn&5Aj{A^cV^^}(7w#p;HG&Q)EaN~~EqrE1qKrMAc&WXIE;>@<&)5;gD2?={Xf@Mvn@OJKw=8Mgn z!JUFMwD+s==JpjhroT&d{$kQAy%+d`a*XxDEVxy3`NHzmITrE`o!;5ClXNPb4t*8P zzAivdr{j_v!=9!^?T3y?gzmqDWX6mkzhIzJ-3S{T5bcCFMr&RPDryMcdwbBuZbsgN zGrp@^i?rcfN7v0NKGzDPGE#4yszxu=I_`MI%Z|10nFjU-UjQXXA?k8Pk|OE<(?ae) zE%vG#eZAlj*E7_3dx#Zz4kMLj>H^;}33UAankJiDy5ZvEhrjr`!9eMD8COp}U*hP+ zF}KIYx@pkccIgyxFm#LNw~G&`;o&5)2`5aogs`1~7cMZQ7zj!%L4E`2yzlQN6REX20&O<9 zKV6fyr)TScJPPzNTC2gL+0x#=u>(({{D7j)c-%tvqls3#Y?Z1m zV5WUE)zdJ{$p>yX;^P!UcXP?UD~YM;IRa#Rs5~l+*$&nO(;Ers`G=0D!twR(0GF@c zHl9E5DQI}Oz74n zfKP>&$q0($T4y$6w(p=ERAFh+>n%iaeRA%!T%<^+pg?M)@ucY<&59$x9M#n+V&>}=nO9wCV{O~lg&v#+jcUj(tQ z`0u1YH)-`U$15a{pBkGyPL0THv1P|4e@pf@3IBZS4dVJPo#H>pWq%Lr0YS-SeWash z8R7=jb28KPMI|_lo#GEO|5B?N_e``H*23{~a!AmUJ+fb4HX-%QI@lSEUxKlGV7z7Q zSKw@-TR>@1RL%w{x}dW#k1NgW+q4yt2Xf1J62Bx*O^WG8OJ|FqI4&@d3_o8Id@*)4 zYrk=>@!wv~mh7YWv*bZhxqSmFh2Xq)o=m;%n$I?GSz49l1$xRpPu_^N(vZ>*>Z<04 z2+rP70oM=NDysd!@fQdM2OcyT?3T^Eb@lIC-UG=Bw{BjQ&P`KCv$AcJ;?`vdZ4){d z&gkoUK{$!$$K`3*O-jyM1~p-7T*qb)Ys>Myt^;#1&a%O@x8A+E>! zY8=eD`ZG)LVagDLBeHg>=atOG?Kr%h4B%E6m@J^C+U|y)XX@f z8oyJDW|9g=<#f<{JRr{y#~euMnv)`7j=%cHWLc}ngjq~7k**6%4u>Px&W%4D94(r* z+akunK}O0DC2A%Xo9jyF;DobX?!1I(7%}@7F>i%&nk*LMO)bMGg2N+1iqtg+r(70q zF5{Msgsm5GS7DT`kBsjMvOrkx&|EU!{{~gL4d2MWrAT=KBQ-^zQCUq{5PD1orxlIL zq;CvlWx#f1NWvh`hg011I%?T_s!e38l*lWVt|~z-PO4~~1g)SrJ|>*tXh=QfXT)%( z+ex+inPvD&O4Ur;JGz>$sUOnWdpSLcm1X%aQDw4{dB!cnj`^muI$CJ2%p&-kULVCE z>$eMR36kN$wCPR+OFDM3-U(VOrp9k3)lI&YVFqd;Kpz~K)@Fa&FRw}L(SoD z9B4a+hQzZT-BnVltst&=kq6Y(f^S4hIGNKYBgMxGJ^;2yrO}P3;r)(-I-CZ)26Y6? z&rzHI_1GCvGkgy-t1E;r^3Le30|%$ebDRu2+gdLG)r=A~Qz`}~&L@aGJ{}vVs_GE* zVUjFnzHiXfKQbpv&bR&}l2bzIjAooB)=-XNcYmrGmBh(&iu@o!^hn0^#}m2yZZUK8 zufVm7Gq0y`Mj;9b>`c?&PZkU0j4>IL=UL&-Lp3j&47B5pAW4JceG{!XCA)kT<%2nqCxj<)uy6XR_uws~>_MEKPOpAQ!H zkn>FKh)<9DwwS*|Y(q?$^N!6(51O0 z^JM~Ax{AI1Oj$fs-S5d4T7Z_i1?{%0SsIuQ&r8#(JA=2iLcTN+?>wOL532%&dMYkT z*T5xepC+V6zxhS@vNbMoi|i)=rpli@R9~P!39tWbSSb904ekv7D#quKbgFEMTb48P zuq(VJ+&L8aWU(_FCD$3^uD!YM%O^K(dvy~Wm2hUuh6bD|#(I39Xt>N1Y{ZqXL`Fg6 zKQ?T2htHN!(Bx;tV2bfTtIj7e)liN-29s1kew>v(D^@)#v;}C4-G=7x#;-dM4yRWm zyY`cS21ulzMK{PoaQ6xChEZ}o_#}X-o}<&0)$1#3we?+QeLt;aVCjeA)hn!}UaKt< zat1fHEx13y-rXNMvpUUmCVzocPmN~-Y4(YJvQ#db)4|%B!rBsgAe+*yor~}FrNH08 z3V!97S}D7d$zbSD{$z;@IYMxM6aHdypIuS*pr_U6;#Y!_?0i|&yU*@16l z*dcMqDQgfNBf}?quiu4e>H)yTVfsp#f+Du0@=Kc41QockXkCkvu>FBd6Q+@FL!(Yx z2`YuX#eMEiLEDhp+9uFqME_E^faV&~9qjBHJkIp~%$x^bN=N)K@kvSVEMdDuzA0sn z88CBG?`RX1@#hQNd`o^V{37)!w|nA)QfiYBE^m=yQKv-fQF+UCMcuEe1d4BH7$?>b zJl-r9@0^Ie=)guO1vOd=i$_4sz>y3x^R7n4ED!5oXL3@5**h(xr%Hv)_gILarO46q+MaDOF%ChaymKoI6JU5Pg;7#2n9-18|S1;AK+ zgsn6;k6-%!QD>D?cFy}8F;r@z8H9xN1jsOBw2vQONVqBVEbkiNUqgw~*!^##ht>w0 zUOykwH=$LwX2j&nLy=@{hr)2O&-wm-NyjW7n~Zs9UlH;P7iP3 zI}S(r0YFVYacnKH(+{*)Tbw)@;6>%=&Th=+Z6NHo_tR|JCI8TJiXv2N7ei7M^Q+RM z?9o`meH$5Yi;@9XaNR#jIK^&{N|DYNNbtdb)XW1Lv2k{E>;?F`#Pq|&_;gm~&~Zc9 zf+6ZE%{x4|{YdtE?a^gKyzr}dA>OxQv+pq|@IXL%WS0CiX!V zm$fCePA%lU{%pTKD7|5NJHeXg=I0jL@$tOF@K*MI$)f?om)D63K*M|r`gb9edD1~Y zc|w7N)Y%do7=0{RC|AziW7#am$)9jciRJ?IWl9PE{G3U+$%FcyKs_0Cgq`=K3@ttV z9g;M!3z~f_?P%y3-ph%vBMeS@p7P&Ea8M@97+%XEj*(1E6vHj==d zjsoviB>j^$_^OI_DEPvFkVo(BGRo%cJeD){6Uckei=~1}>sp299|IRjhXe)%?uP0I zF5+>?0#Ye}T^Y$u_rc4=lPcq4K^D(TZG-w30-YiEM=dcK+4#o*>lJ8&JLi+3UcpZk z!^?95S^C0ja^jwP`|{<+3cBVog$(mRdQmadS+Vh~z zS@|P}=|z3P6uS+&@QsMp0no9Od&27O&14zHXGAOEy zh~OKpymK5C%;LLb467@KgIiVwYbYd6wFxI{0-~MOGfTq$nBTB!{SrWmL9Hs}C&l&l#m?s*{tA?BHS4mVKHAVMqm63H<|c5n0~k)-kbg zXidai&9ZUy0~WFYYKT;oe~rytRk?)r8bptITsWj(@HLI;@=v5|XUnSls7$uaxFRL+ zRVMGuL3w}NbV1`^=Pw*0?>bm8+xfeY(1PikW*PB>>Tq(FR`91N0c2&>lL2sZo5=VD zQY{>7dh_TX98L2)n{2OV=T10~*YzX27i2Q7W86M4$?gZIXZaBq#sA*{PH8){|GUi;oM>e?ua7eF4WFuFYZSG| zze?srg|5Ti8Og{O zeFxuw9!U+zhyk?@w zjsA6(oKD=Ka;A>Ca)oPORxK+kxH#O@zhC!!XS4@=swnuMk>t+JmLmFiE^1aX3f<)D@`%K0FGK^gg1a1j>zi z2KhV>sjU7AX3F$SEqrXSC}fRx64GDoc%!u2Yag68Lw@w9v;xOONf@o)Lc|Uh3<21ctTYu-mFZuHk*+R{GjXHIGq3p)tFtQp%TYqD=j1&y)>@zxoxUJ!G@ zgI0XKmP6MNzw>nRxK$-Gbzs}dyfFzt>#5;f6oR27ql!%+{tr+(`(>%51|k`ML} zY4eE)Lxq|JMas(;JibNQds1bUB&r}ydMQXBY4x(^&fY_&LlQC)3hylc$~8&~|06-D z#T+%66rYbHX%^KuqJED_wuGB+=h`nWA!>1n0)3wZrBG3%`b^Ozv6__dNa@%V14|!D zQ?o$z5u0^8`giv%qE!BzZ!3j;BlDlJDk)h@9{nSQeEk!z9RGW) z${RSF3phEM*ce*>Xdp}585vj$|40=&S{S-GTiE?Op*vY&Lvr9}BO$XWy80IF+6@%n z5*2ueT_g@ofP#u5pxb7n*fv^Xtt7&?SRc{*2Ka-*!BuOpf}neHGCiHy$@Ka1^Dint z;DkmIL$-e)rj4o2WQV%Gy;Xg(_Bh#qeOsTM2f@KEe~4kJ8kNLQ+;(!j^bgJMcNhvklP5Z6I+9Fq@c&D~8Fb-4rmDT!MB5QC{Dsb;BharP*O;SF4& zc$wj-7Oep7#$WZN!1nznc@Vb<_Dn%ga-O#J(l=OGB`dy=Sy&$(5-n3zzu%d7E#^8`T@}V+5B;PP8J14#4cCPw-SQTdGa2gWL0*zKM z#DfSXs_iWOMt)0*+Y>Lkd=LlyoHjublNLefhKBv@JoC>P7N1_#> zv=mLWe96%EY;!ZGSQDbZWb#;tzqAGgx~uk+-$+2_8U`!ypbwXl z^2E-FkM1?lY@yt8=J3%QK+xaZ6ok=-y%=KXCD^0r!5vUneW>95PzCkOPO*t}p$;-> ze5j-BLT_;)cZQzR2CEsm@rU7GZfFtdp*a|g4wDr%8?2QkIGasRfDWT-Dvy*U{?IHT z*}wGnzdlSptl#ZF^sf)KT|BJs&kLG91^A6ls{CzFprZ6-Y!V0Xysh%9p%iMd7HLsS zN+^Un$tDV)T@i!v?3o0Fsx2qI(AX_$dDkBzQ@fRM%n zRXk6hb9Py#JXUs+7)w@eo;g%QQ95Yq!K_d=z{0dGS+pToEI6=Bo8+{k$7&Z zo4>PH(`ce8E-Ps&uv`NQ;U$%t;w~|@E3WVOCi~R4oj5wP?%<*1C%}Jq%a^q~T7u>K zML5AKfQDv6>PuT`{SrKHRAF+^&edg6+5R_#H?Lz3iGoWo#PCEd0DS;)2U({{X#zU^ zw_xv{4x7|t!S)>44J;KfA|DC?;uQ($l+5Vp7oeqf7{GBF9356nx|&B~gs+@N^gSdd zvb*>&W)|u#F{Z_b`f#GVtQ`pYv3#||N{xj1NgB<#=Odt6{eB%#9RLt5v zIi|0u70`#ai}9fJjKv7dE!9ZrOIX!3{$z_K5FBd-Kp-&e4(J$LD-)NMTp^_pB`RT; zftVVlK2g@+1Ahv2$D){@Y#cL#dUj9*&%#6 zd2m9{1NYp>)6=oAvqdCn5#cx{AJ%S8skUgMglu2*IAtd+z1>B&`MuEAS(D(<6X#Lj z?f4CFx$)M&$=7*>9v1ER4b6!SIz-m0e{o0BfkySREchp?WdVPpQCh!q$t>?rL!&Jg zd#heM;&~A}VEm8Dvy&P|J*eAV&w!&Nx6HFV&B8jJFVTmgLaswn!cx$&%JbTsloz!3 zMEz1d`k==`Ueub_JAy_&`!ogbwx27^ZXgFNAbx=g_I~5nO^r)}&myw~+yY*cJl4$I znNJ32M&K=0(2Dj_>@39`3=FX!v3nZHno_@q^!y}%(yw0PqOo=);6Y@&ylVe>nMOZ~ zd>j#QQSBn3oaWd;qy$&5(5H$Ayi)0haAYO6TH>FR?rhqHmNOO+(})NB zLI@B@v0)eq!ug`>G<@htRlp3n!EpU|n+G+AvXFrWSUsLMBfL*ZB`CRsIVHNTR&b?K zxBgsN0BjfB>UVcJ|x%=-zb%OV7lmZc& zxiupadZVF7)6QuhoY;;FK2b*qL0J-Rn-8!X4ZY$-ZSUXV5DFd7`T41c(#lAeLMoeT z4%g655v@7AqT!i@)Edt5JMbN(=Q-6{=L4iG8RA%}w;&pKmtWvI4?G9pVRp|RTw`g0 zD5c12B&A2&P6Ng~8WM2eIW=wxd?r7A*N+&!Be7PX3s|7~z=APxm=A?5 zt>xB4WG|*Td@VX{Rs)PV0|yK`oI3^xn(4c_j&vgxk_Y3o(-`_5o`V zRTghg6%l@(qodXN;dB#+OKJEEvhfcnc#BeO2|E(5df-!fKDZ!%9!^BJ_4)9P+9Dq5 zK1=(v?KmIp34r?z{NEWnLB3Px{XYwy-akun4F7xTRr2^zeYW{gcK9)>aJDdU5;w5@ zak=<+-PLH-|04pelTb%ULpuuuJC7DgyT@D|p{!V!0v3KpDnRjANN12q6SUR3mb9<- z>2r~IApQGhstZ!3*?5V z8#)hJ0TdZg0M-BK#nGFP>$i=qk82DO z7h;Ft!D5E15OgW)&%lej*?^1~2=*Z5$2VX>V{x8SC+{i10BbtUk9@I#Vi&hX)q
Q!LwySI{Bnv%Sm)yh{^sSVJ8&h_D-BJ_YZe5eCaAWU9b$O2c z$T|{vWVRtOL!xC0DTc(Qbe`ItNtt5hr<)VijD0{U;T#bUEp381_y`%ZIav?kuYG{iyYdEBPW=*xNSc;Rlt6~F4M`5G+VtOjc z*0qGzCb@gME5udTjJA-9O<&TWd~}ysBd(eVT1-H82-doyH9RST)|+Pb{o*;$j9Tjs zhU!IlsPsj8=(x3bAKJTopW3^6AKROHR^7wZ185wJGVhA~hEc|LP;k7NEz-@4p5o}F z`AD6naG3(n=NF9HTH81=F+Q|JOz$7wm9I<+#BSmB@o_cLt2GkW9|?7mM;r!JZp89l zbo!Hp8=n!XH1{GwaDU+k)pGp`C|cXkCU5%vcH)+v@0eK>%7gWxmuMu9YLlChA|_D@ zi#5zovN_!a-0?~pUV-Rj*1P)KwdU-LguR>YM&*Nen+ln8Q$?WFCJg%DY%K}2!!1FE zDv-A%Cbwo^p(lzac&_TZ-l#9kq`mhLcY3h9ZTUVCM(Ad&=EriQY5{jJv<5K&g|*Lk zgV%ILnf1%8V2B0E&;Sp4sYbYOvvMebLwYwzkRQ#F8GpTQq#uv=J`uaSJ34OWITeSGo6+-8Xw znCk*n{kdDEi)Hi&u^)~cs@iyCkFWB2SWZU|Uc%^43ZIZQ-vWNExCCtDWjqHs;;tWf$v{}0{p0Rvxkq``)*>+Akq%|Na zA`@~-Vfe|+(AIlqru+7Ceh4nsVmO9p9jc8}HX^W&ViBDXT+uXbT#R#idPn&L>+#b6 zflC-4C5-X;kUnR~L>PSLh*gvL68}RBsu#2l`s_9KjUWRhiqF`j)`y`2`YU(>3bdBj z?>iyjEhe-~$^I5!nn%B6Wh+I`FvLNvauve~eX<+Ipl&04 zT}};W&1a3%W?dJ2=N#0t?e+aK+%t}5q%jSLvp3jZ%?&F}nOOWr>+{GFIa%wO_2`et z=JzoRR~}iKuuR+azPI8;Gf9)z3kyA4EIOSl!sRR$DlW}0>&?GbgPojmjmnln;cTqCt=ADbE zZ8GAnoM+S1(5$i8^O4t`ue;vO4i}z0wz-QEIVe5_u03;}-!G1NyY8;h^}y;tzY}i5 zqQr#Ur3Fy8sSa$Q0ys+f`!`+>9WbvU_I`Sj;$4{S>O3?#inLHCrtLy~!s#WXV=oVP zeE93*Nc`PBi4q@%Ao$x4lw9vLHM!6mn3-b_cebF|n-2vt-zYVF_&sDE--J-P;2WHo z+@n2areE0o$LjvjlV2X7ZU@j+`{*8zq`JR3gKF#EW|#+{nMyo-a>nFFTg&vhyT=b} zDa8+v0(Dgx0yRL@ZXOYIlVSZ0|MFizy0VPW8;AfA5|pe!#j zX}Py^8fl5SyS4g1WSKKtnyP+_PoOwMMwu`(i@Z)diJp~U54*-miOchy7Z35eL>^M z4p<-aIxH4VUZgS783@H%M7P9hX>t{|RU7$n4T(brCG#h9e9p! z+o`i;EGGq3&pF;~5V~eBD}lC)>if$w%Vf}AFxGqO88|ApfHf&Bvu+xdG)@vuF}Yvk z)o;~k-%+0K0g+L`Wala!$=ZV|z$e%>f0%XoLib%)!R^RoS+{!#X?h-6uu zF&&KxORdZU&EwQFITIRLo(7TA3W}y6X{?Y%y2j0It!ekU#<)$qghZtpcS>L3uh`Uj z7GY;6f$9qKynP#oS3$$a{p^{D+0oJQ71`1?OAn_m8)UGZmj3l*ZI)`V-a>MKGGFG< z&^jg#Ok%(hhm>hSrZ5;Qga4u(?^i>GiW_j9%_7M>j(^|Om$#{k+^*ULnEgzW_1gCICtAD^WpC`A z{9&DXkG#01Xo)U$OC(L5Y$DQ|Q4C6CjUKk1UkPj$nXH##J{c8e#K|&{mA*;b$r0E4 zUNo0jthwA(c&N1l=PEe8Rw_8cEl|-eya9z&H3#n`B$t#+aJ03RFMzrV@gowbe8v(c zIFM60^0&lCFO10NU4w@|61xiZ4CVXeaKjd;d?sv52XM*lS8XiVjgWpRB;&U_C0g+`6B5V&w|O6B*_q zsATxL!M}+$He)1eOWECce#eS@2n^xhlB4<_Nn?yCVEQWDs(r`|@2GqLe<#(|&P0U? z$7V5IgpWf09uIf_RazRwC?qEqRaHyL?iiS05UiGesJy%^>-C{{ypTBI&B0-iUYhk> zIk<5xpsuV@g|z(AZD+C-;A!fTG=df1=<%nxy(a(IS+U{ME4ZbDEBtcD_3V=icT6*_ z)>|J?>&6%nvHhZERBtjK+s4xnut*@>GAmA5m*OTp$!^CHTr}vM4n(X1Q*;{e-Rd2BCF-u@1ZGm z!S8hJ6L=Gl4T_SDa7Xx|-{4mxveJg=ctf`BJ*fy!yF6Dz&?w(Q_6B}WQVtNI!BVBC zKfX<>7vd6C96}XAQmF-Jd?1Q4eTfRB3q7hCh0f!(JkdWT5<{iAE#dKy*Jxq&3a1@~ z8C||Dn2mFNyrUV|<-)C^_y7@8c2Fz+2jrae9deBDu;U}tJ{^xAdxCD248(k;dCJ%o z`y3sADe>U%suxwwv~8A1+R$VB=Q?%U?4joI$um;aH+eCrBqpn- z%79D_7rb;R-;-9RTrwi9dPlg8&@tfWhhZ(Vx&1PQ+6(huX`;M9x~LrW~~#3{j0Bh2kDU$}@!fFQej4VGkJv?M4rU^x!RU zEwhu$!CA_iDjFjrJa`aocySDX16?~;+wgav;}Zut6Mg%C4>}8FL?8)Kgwc(Qlj{@#2Pt0?G`$h7P#M+qoXtlV@d}%c&OzO+QYKK`kyXaK{U(O^2DyIXCZlNQjt0^8~8JzNGrIxhj}}M z&~QZlbx%t;MJ(Vux;2tgNKGlAqphLq%pd}JG9uoVHUo?|hN{pLQ6Em%r*+7t^<);X zm~6=qChlNAVXNN*Sow->*4;}T;l;D1I-5T{Bif@4_}=>l`tK;qqDdt5zvisCKhMAH z#r}`)7VW?LZqfdmXQ%zo5bJ00{Xb9^YKrk0Nf|oIW*K@(=`o2Vndz}ZDyk{!u}PVx zzd--+_WC*U{~DH3{?GI64IB+@On&@9X>EUAo&L+G{L^dozaI4C3G#2wr~hseW@K&g zKWs{uHu-9Je!3;4pE>eBltKUXb^*hG8I&413)$J&{D4N%7PcloU6bn%jPxJyQL?g* z9g+YFFEDiE`8rW^laCNzQmi7CTnPfwyg3VDHRAl>h=In6jeaVOP@!-CP60j3+#vpL zEYmh_oP0{-gTe7Or`L6x)6w?77QVi~jD8lWN@3RHcm80iV%M1A!+Y6iHM)05iC64tb$X2lV_%Txk@0l^hZqi^%Z?#- zE;LE0uFx)R08_S-#(wC=dS&}vj6P4>5ZWjhthP=*Hht&TdLtKDR;rXEX4*z0h74FA zMCINqrh3Vq;s%3MC1YL`{WjIAPkVL#3rj^9Pj9Ss7>7duy!9H0vYF%>1jh)EPqvlr6h%R%CxDsk| z!BACz7E%j?bm=pH6Eaw{+suniuY7C9Ut~1cWfOX9KW9=H><&kQlinPV3h9R>3nJvK z4L9(DRM=x;R&d#a@oFY7mB|m8h4692U5eYfcw|QKwqRsshN(q^v$4$)HgPpAJDJ`I zkqjq(8Cd!K!+wCd=d@w%~e$=gdUgD&wj$LQ1r>-E=O@c ze+Z$x{>6(JA-fNVr)X;*)40Eym1TtUZI1Pwwx1hUi+G1Jlk~vCYeXMNYtr)1?qwyg zsX_e*$h?380O00ou?0R@7-Fc59o$UvyVs4cUbujHUA>sH!}L54>`e` zHUx#Q+Hn&Og#YVOuo*niy*GU3rH;%f``nk#NN5-xrZ34NeH$l`4@t);4(+0|Z#I>Y z)~Kzs#exIAaf--65L0UHT_SvV8O2WYeD>Mq^Y6L!Xu8%vnpofG@w!}R7M28?i1*T&zp3X4^OMCY6(Dg<-! zXmcGQrRgHXGYre7GfTJ)rhl|rs%abKT_Nt24_Q``XH{88NVPW+`x4ZdrMuO0iZ0g` z%p}y};~T5gbb9SeL8BSc`SO#ixC$@QhXxZ=B}L`tP}&k?1oSPS=4%{UOHe0<_XWln zwbl5cn(j-qK`)vGHY5B5C|QZd5)W7c@{bNVXqJ!!n$^ufc?N9C-BF2QK1(kv++h!>$QbAjq)_b$$PcJdV+F7hz0Hu@ zqj+}m0qn{t^tD3DfBb~0B36|Q`bs*xs|$i^G4uNUEBl4g;op-;Wl~iThgga?+dL7s zUP(8lMO?g{GcYpDS{NM!UA8Hco?#}eNEioRBHy4`mq!Pd-9@-97|k$hpEX>xoX+dY zDr$wfm^P&}Wu{!%?)U_(%Mn79$(ywvu*kJ9r4u|MyYLI_67U7%6Gd_vb##Nerf@>& z8W11z$$~xEZt$dPG}+*IZky+os5Ju2eRi;1=rUEeIn>t-AzC_IGM-IXWK3^6QNU+2pe=MBn4I*R@A%-iLDCOHTE-O^wo$sL_h{dcPl=^muAQb`_BRm};=cy{qSkui;`WSsj9%c^+bIDQ z0`_?KX0<-=o!t{u(Ln)v>%VGL z0pC=GB7*AQ?N7N{ut*a%MH-tdtNmNC+Yf$|KS)BW(gQJ*z$d{+{j?(e&hgTy^2|AR9vx1Xre2fagGv0YXWqtNkg*v%40v?BJBt|f9wX5 z{QTlCM}b-0{mV?IG>TW_BdviUKhtosrBqdfq&Frdz>cF~yK{P@(w{Vr7z2qKFwLhc zQuogKO@~YwyS9%+d-zD7mJG~@?EFJLSn!a&mhE5$_4xBl&6QHMzL?CdzEnC~C3$X@ zvY!{_GR06ep5;<#cKCSJ%srxX=+pn?ywDwtJ2{TV;0DKBO2t++B(tIO4)Wh`rD13P z4fE$#%zkd=UzOB74gi=-*CuID&Z3zI^-`4U^S?dHxK8fP*;fE|a(KYMgMUo`THIS1f!*6dOI2 zFjC3O=-AL`6=9pp;`CYPTdVX z8(*?V&%QoipuH0>WKlL8A*zTKckD!paN@~hh zmXzm~qZhMGVdQGd=AG8&20HW0RGV8X{$9LldFZYm zE?}`Q3i?xJRz43S?VFMmqRyvWaS#(~Lempg9nTM$EFDP(Gzx#$r)W&lpFKqcAoJh-AxEw$-bjW>`_+gEi z2w`99#UbFZGiQjS8kj~@PGqpsPX`T{YOj`CaEqTFag;$jY z8_{Wzz>HXx&G*Dx<5skhpETxIdhKH?DtY@b9l8$l?UkM#J-Snmts7bd7xayKTFJ(u zyAT&@6cAYcs{PBfpqZa%sxhJ5nSZBPji?Zlf&}#L?t)vC4X5VLp%~fz2Sx<*oN<7` z?ge=k<=X7r<~F7Tvp9#HB{!mA!QWBOf%EiSJ6KIF8QZNjg&x~-%e*tflL(ji_S^sO ztmib1rp09uon}RcsFi#k)oLs@$?vs(i>5k3YN%$T(5Or(TZ5JW9mA6mIMD08=749$ z!d+l*iu{Il7^Yu}H;lgw=En1sJpCKPSqTCHy4(f&NPelr31^*l%KHq^QE>z>Ks_bH zjbD?({~8Din7IvZeJ>8Ey=e;I?thpzD=zE5UHeO|neioJwG;IyLk?xOz(yO&0DTU~ z^#)xcs|s>Flgmp;SmYJ4g(|HMu3v7#;c*Aa8iF#UZo7CvDq4>8#qLJ|YdZ!AsH%^_7N1IQjCro

K7UpUK$>l@ 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 E2O(~oYXATM diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2ea3535d..ff23a68d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 4f906e0c..23d15a93 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,81 +15,115 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,88 +132,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 107acd32..db3a6ac2 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,8 +13,10 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +27,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -56,32 +59,34 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal From 1d77898ee793a39e7cdd3a8acbd9a7cb12467292 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:40:06 -0600 Subject: [PATCH 82/84] Remove `.gitattributes` --- .gitattributes | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 710d818f..00000000 --- a/.gitattributes +++ /dev/null @@ -1,7 +0,0 @@ -# Handle line endings automatically for files detected as text and leave all files detected as binary untouched. -* text=auto - -# Declare files that should always have CRLF line endings at checkout. -*.sln text eol=crlf -*.bat text eol=crlf -*.cmd text eol=crlf From 0b8f28aba489db35dca3a68b8a44bfddabf79704 Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:13:36 -0600 Subject: [PATCH 83/84] Update Gradle wrapper --- gradle/wrapper/gradle-wrapper.jar | Bin 43764 -> 45457 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 5 +- gradlew.bat | 187 +++++++++++------------ 4 files changed, 95 insertions(+), 99 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 1b33c55baabb587c669f562ae36f953de2481846..8bdaf60c75ab801e22807dde59e12a8735a34077 100644 GIT binary patch delta 37256 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 delta 35551 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ff23a68d..2e111328 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 23d15a93..adff685a 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -114,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -172,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -212,7 +210,6 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" diff --git a/gradlew.bat b/gradlew.bat index db3a6ac2..e509b2dd 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,94 +1,93 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH= - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega From b692f8ecc8eb0793327cee710063f4c892ec826e Mon Sep 17 00:00:00 2001 From: Jacob Peterson <14127217+Petersoj@users.noreply.github.com> Date: Thu, 9 Oct 2025 13:43:04 -0600 Subject: [PATCH 84/84] Add `.github/FUNDING.yml` --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..55ede737 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: Petersoj \ No newline at end of file