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

net.jacobpeterson.alpaca.websocket.client.AlpacaWebsocketClient Maven / Gradle / Ivy

package net.jacobpeterson.alpaca.websocket.client;

import com.google.common.base.Preconditions;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSyntaxException;
import net.jacobpeterson.abstracts.websocket.client.WebsocketClient;
import net.jacobpeterson.abstracts.websocket.listener.StreamListener;
import net.jacobpeterson.abstracts.websocket.message.StreamMessage;
import net.jacobpeterson.abstracts.websocket.message.StreamMessageType;
import net.jacobpeterson.alpaca.websocket.listener.AlpacaStreamListener;
import net.jacobpeterson.alpaca.websocket.message.AlpacaStreamMessageType;
import net.jacobpeterson.domain.alpaca.websocket.AlpacaStreamMessage;
import net.jacobpeterson.domain.alpaca.websocket.account.AccountUpdateMessage;
import net.jacobpeterson.domain.alpaca.websocket.authorization.AuthorizationMessage;
import net.jacobpeterson.domain.alpaca.websocket.listening.ListeningMessage;
import net.jacobpeterson.domain.alpaca.websocket.trade.TradeUpdateMessage;
import net.jacobpeterson.util.gson.GsonUtil;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.websocket.DeploymentException;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * The type Alpaca websocket client.
 */
public class AlpacaWebsocketClient implements WebsocketClient {

    /** The logger. */
    private static final Logger LOGGER = LogManager.getLogger(AlpacaWebsocketClient.class);

    /** The constant STREAM_KEY. */
    private static final String STREAM_KEY = "stream";

    /** The key id. */
    private String keyId;

    /** The secret. */
    private String secret;

    /** The Base api url. */
    private String baseAPIURL;

    /** The observers. */
    private List listeners;

    /** The client end point. */
    private AlpacaWebsocketClientEndpoint alpacaWebsocketClientEndpoint;

    /** The Authenticated. */
    private boolean authenticated;

    /**
     * Instantiates a new Alpaca websocket client.
     *
     * @param keyId      the key id
     * @param secret     the secret
     * @param baseAPIURL the base apiurl
     */
    public AlpacaWebsocketClient(String keyId, String secret, String baseAPIURL) {
        this.keyId = keyId;
        this.secret = secret;
        this.baseAPIURL = baseAPIURL.replace("https", "wss") + "/stream";

        this.listeners = new ArrayList<>();
    }

    @Override
    public void addListener(StreamListener streamListener) {
        Preconditions.checkState(streamListener instanceof AlpacaStreamListener);

        if (listeners.isEmpty()) {
            connect();
        }

        listeners.add((AlpacaStreamListener) streamListener);

        submitStreamRequestUpdate();
    }

    @Override
    public void removeListener(StreamListener streamListener) {
        Preconditions.checkState(streamListener instanceof AlpacaStreamListener);

        listeners.remove(streamListener);

        submitStreamRequestUpdate();

        if (listeners.isEmpty()) {
            disconnect();
        }
    }

    @Override
    public void connect() {
        LOGGER.info("Connecting...");

        try {
            alpacaWebsocketClientEndpoint = new AlpacaWebsocketClientEndpoint(this, new URI(baseAPIURL));
            alpacaWebsocketClientEndpoint.connect();

            LOGGER.info("Connected.");
        } catch (URISyntaxException | DeploymentException | IOException e) {
            LOGGER.throwing(e);
        }
    }

    @Override
    public void disconnect() {
        LOGGER.info("Disconnecting...");

        try {
            alpacaWebsocketClientEndpoint.getUserSession().close();

            LOGGER.info("Disconnected.");
        } catch (IOException e) {
            LOGGER.throwing(e);
        }
    }

    @Override
    public void sendAuthenticationMessage() {
        /* Format of message is:
         *  {
         *      "action": "authenticate",
         *      "data": {
         *          "key_id": "{YOUR_API_KEY_ID}",
         *          "secret_key": "{YOUR_API_SECRET_KEY}"
         *      }
         *  }
         */

        JsonObject authRequest = new JsonObject();
        authRequest.addProperty("action", "authenticate");

        JsonObject payload = new JsonObject();
        payload.addProperty("key_id", keyId);
        payload.addProperty("secret_key", secret);

        authRequest.add("data", payload);

        alpacaWebsocketClientEndpoint.sendMessage(authRequest.toString());
    }

    @Override
    public void handleWebsocketMessage(String message) {
        JsonElement messageJsonElement = GsonUtil.JSON_PARSER.parse(message);

        Preconditions.checkState(messageJsonElement instanceof JsonObject);

        JsonObject messageJsonObject = messageJsonElement.getAsJsonObject();
        JsonElement streamJsonElement = messageJsonObject.get(STREAM_KEY);

        if (streamJsonElement instanceof JsonPrimitive) {
            try {
                AlpacaStreamMessageType alpacaStreamMessageType = GsonUtil.GSON.fromJson(streamJsonElement,
                        AlpacaStreamMessageType.class);

                switch (alpacaStreamMessageType) {
                    case LISTENING:
                        ListeningMessage listeningMessage = GsonUtil.GSON.fromJson(messageJsonObject,
                                ListeningMessage.class);
                        sendStreamMessageToListeners(alpacaStreamMessageType, listeningMessage);

                        LOGGER.debug(listeningMessage);
                        break;
                    case AUTHORIZATION:
                        AuthorizationMessage authorizationMessage = GsonUtil.GSON.fromJson(messageJsonObject,
                                AuthorizationMessage.class);
                        sendStreamMessageToListeners(alpacaStreamMessageType, authorizationMessage);

                        authenticated = isAuthorizationMessageSuccess(authorizationMessage);

                        LOGGER.debug(authorizationMessage);
                        break;
                    case TRADE_UPDATES:
                        sendStreamMessageToListeners(alpacaStreamMessageType, GsonUtil.GSON.fromJson(messageJsonObject,
                                TradeUpdateMessage.class));
                        break;
                    case ACCOUNT_UPDATES:
                        sendStreamMessageToListeners(alpacaStreamMessageType, GsonUtil.GSON.fromJson(messageJsonObject,
                                AccountUpdateMessage.class));
                        break;
                    default:
                        LOGGER.error("Unhandled stream type: " + alpacaStreamMessageType);
                }
            } catch (JsonSyntaxException e) {
                LOGGER.throwing(e);
            }
        } else {
            LOGGER.error("Unknown stream message: " + messageJsonObject);
        }
    }

    @Override
    public void sendStreamMessageToListeners(StreamMessageType streamMessageType, StreamMessage streamMessage) {
        Preconditions.checkState(streamMessageType instanceof AlpacaStreamMessageType);
        Preconditions.checkState(streamMessage instanceof AlpacaStreamMessage);

        AlpacaStreamMessageType alpacaStreamMessageType = (AlpacaStreamMessageType) streamMessageType;
        AlpacaStreamMessage alpacaStreamMessage = (AlpacaStreamMessage) streamMessage;

        for (AlpacaStreamListener alpacaStreamListener : listeners) {
            if (alpacaStreamListener.getStreamMessageTypes() == null ||
                    alpacaStreamListener.getStreamMessageTypes().isEmpty() ||
                    alpacaStreamListener.getStreamMessageTypes().contains(alpacaStreamMessageType)) {
                alpacaStreamListener.onStreamUpdate(alpacaStreamMessageType, alpacaStreamMessage);
            }
        }
    }

    @Override
    public boolean isConnected() {
        return alpacaWebsocketClientEndpoint.getUserSession().isOpen();
    }

    @Override
    public boolean isAuthenticated() {
        return authenticated;
    }

    /**
     * Is authorization message success boolean.
     *
     * @param authorizationMessage the authorization message
     *
     * @return the boolean
     */
    private boolean isAuthorizationMessageSuccess(AuthorizationMessage authorizationMessage) {
        return authorizationMessage.getData().getStatus().equalsIgnoreCase("authorized") &&
                authorizationMessage.getData().getAction().equalsIgnoreCase("authenticate");
    }

    /**
     * Submit stream request.
     */
    private void submitStreamRequestUpdate() {
        // Stream request example:
        // {
        //     "action": "listen",
        //     "data": {
        //         "streams": ["account_updates", "trade_updates"]
        //     }
        // }

        JsonObject streamRequestJsonObject = new JsonObject();
        streamRequestJsonObject.addProperty("action", "listen");

        JsonArray streamsJsonArray = new JsonArray();
        getRegisteredMessageTypes().forEach(alpacaStreamMessageType ->
                streamsJsonArray.add(alpacaStreamMessageType.getAPIName()));

        JsonObject dataJsonObject = new JsonObject();
        dataJsonObject.add("streams", streamsJsonArray);

        streamRequestJsonObject.add("data", dataJsonObject);

        alpacaWebsocketClientEndpoint.sendMessage(streamRequestJsonObject.toString());

        LOGGER.info("Requested subscriptions to update to " + streamsJsonArray);
    }

    /**
     * Gets the registered message types.
     *
     * @return the registered message types
     */
    public Set getRegisteredMessageTypes() {
        Set registeredStreamMessageTypes = new HashSet<>();

        for (AlpacaStreamListener alpacaStreamListener : listeners) {
            Set alpacaStreamMessageTypes = alpacaStreamListener.getStreamMessageTypes();

            // if its empty, assume they want everything
            Set streamMessageTypesToAdd = alpacaStreamMessageTypes == null ? new HashSet<>() :
                    alpacaStreamMessageTypes.stream().filter(AlpacaStreamMessageType::isAPISubscribable)
                            .collect(Collectors.toSet());

            registeredStreamMessageTypes.addAll(streamMessageTypesToAdd);
        }

        return registeredStreamMessageTypes;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy